Files
Projektarbeit-MYP/backend/templates/base.html
Till Tomczak 124953049b Title: Enhanced Data Management and Job Queue System Integration
🎉 New Feature: Integrated advanced data management capabilities with improved job queue system for seamless workflow.

📚 The updated data management module now offers robust features such as data validation, normalization, and efficient storage using optimized database queries. This ensures accurate and consistent data handling across the application.

💄 Additionally, the job queue system has been upgraded to handle complex tasks more efficiently, reducing latency and improving overall
2025-06-16 00:58:43 +02:00

921 lines
43 KiB
HTML

<!DOCTYPE html>
<html lang="de" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="MYP Platform - Mercedes-Benz 3D Druck Management System">
<meta name="author" content="Mercedes-Benz Group AG">
<meta name="robots" content="noindex, nofollow">
<meta id="theme-color" name="theme-color" content="#ffffff">
<meta name="csrf-token" content="{{ csrf_token() if csrf_token else session.get('_csrf_token', '') }}">
<title>{% block title %}MYP - Mercedes-Benz{% endblock %}</title>
<!-- PWA Manifest -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<!-- CSS Bundle -->
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='fontawesome/css/all.min.css') }}" rel="stylesheet">
<!-- Unified Dark/Light Mode System -->
<link href="{{ url_for('static', filename='css/dark-light-unified.css') }}" rel="stylesheet">
<!-- Modern Styles with Glassmorphism -->
<style>
/* Root Variables */
:root {
--glass-bg: rgba(255, 255, 255, 0.7);
--glass-border: rgba(255, 255, 255, 0.2);
--shadow-color: rgba(0, 0, 0, 0.1);
--text-primary: #1e293b;
--text-secondary: #64748b;
}
.dark {
--glass-bg: rgba(30, 41, 59, 0.7);
--glass-border: rgba(255, 255, 255, 0.1);
--shadow-color: rgba(0, 0, 0, 0.3);
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
}
/* Glassmorphism Base */
.glass {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px var(--shadow-color);
border-radius: 20px;
padding: 0.75rem 1.5rem;
margin: 0.5rem;
}
/* Raspberry Pi Performance Optimization */
@media (max-width: 768px), (prefers-reduced-motion: reduce) {
.glass {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: var(--bg-card);
}
* {
transition: none !important;
animation: none !important;
}
}
/* Sticky Navigation */
.navbar-sticky {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 50;
}
/* Main content offset for sticky navbar */
.main-offset {
padding-top: 3rem;
}
/* Smooth transitions */
* {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.dark ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
/* Mobile menu animations */
.mobile-menu {
transform: translateX(-100%);
transition: transform 0.3s ease-in-out;
}
.mobile-menu.active {
transform: translateX(0);
}
/* Active nav item */
.nav-active {
background: rgba(59, 130, 246, 0.1);
border-left: 3px solid #3b82f6;
}
.dark .nav-active {
background: rgba(59, 130, 246, 0.2);
}
/* Hover effects */
.hover-lift {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.hover-lift:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px var(--shadow-color);
}
/* Loading animation */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
</style>
<!-- CSRF-Token-Fix (Kritisch - muss vor anderen Scripts geladen werden) -->
<script src="{{ url_for('static', filename='js/csrf-fix.js') }}"></script>
<!-- Dark Mode Script (Instant) -->
<script>
(function(){
const savedMode = localStorage.getItem('myp-dark-mode');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = savedMode === 'true' || (savedMode === null && prefersDark);
if (isDark) {
document.documentElement.classList.add('dark');
document.getElementById('theme-color')?.setAttribute('content', '#1e293b');
} else {
document.documentElement.classList.remove('dark');
document.getElementById('theme-color')?.setAttribute('content', '#ffffff');
}
})();
</script>
{% block head %}{% endblock %}
</head>
<body class="h-full bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 text-slate-900 dark:text-slate-100">
<!-- Fixed Navbar with Glassmorphism -->
<nav class="navbar-sticky glass">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-12">
<!-- Logo & Brand -->
<div class="flex items-center">
<!-- Mobile Menu Button -->
<button id="mobile-menu-btn" class="lg:hidden p-1.5 rounded-lg hover:bg-white/10 dark:hover:bg-black/10">
<i class="fas fa-bars text-lg"></i>
</button>
<!-- Logo -->
<a href="{{ url_for('dashboard') if current_user.is_authenticated else url_for('index') }}"
class="flex items-center space-x-3 ml-2 lg:ml-0 hover-lift">
<div class="w-8 h-8 bg-white dark:bg-slate-800 rounded-lg shadow-lg p-1.5">
<svg class="w-full h-full text-slate-900 dark:text-white" fill="currentColor" viewBox="0 0 80 80">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
</div>
<div class="hidden sm:block">
<div class="text-base font-bold">MYP</div>
<div class="text-xs text-slate-600 dark:text-slate-400">Mercedes-Benz</div>
</div>
</a>
</div>
<!-- Desktop Navigation -->
{% if current_user.is_authenticated %}
<div class="hidden lg:flex items-center space-x-1 flex-1 justify-center mx-8">
<a href="{{ url_for('dashboard') }}"
class="nav-item flex items-center px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'dashboard' else '' }}">
<i class="fas fa-tachometer-alt mr-2"></i>
<span>Dashboard</span>
</a>
<a href="{{ url_for('printers_page') }}"
class="nav-item flex items-center px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'printers_page' else '' }}">
<i class="fas fa-print mr-2"></i>
<span>Drucker</span>
</a>
<a href="{{ url_for('jobs_page') }}"
class="nav-item flex items-center px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'jobs_page' else '' }}">
<i class="fas fa-tasks mr-2"></i>
<span>Aufträge</span>
</a>
<a href="{{ url_for('calendar.calendar_view') }}"
class="nav-item flex items-center px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'calendar.calendar_view' else '' }}">
<i class="fas fa-calendar mr-2"></i>
<span>Kalender</span>
</a>
<a href="{{ url_for('energy.energy_dashboard') }}"
class="nav-item flex items-center px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'energy.energy_dashboard' else '' }}">
<i class="fas fa-bolt mr-2"></i>
<span>Energie</span>
</a>
<a href="{{ url_for('stats_page') }}"
class="nav-item flex items-center px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'stats_page' else '' }}">
<i class="fas fa-chart-bar mr-2"></i>
<span>Statistiken</span>
</a>
<a href="{{ url_for('guest.guest_request_form') }}"
class="nav-item flex items-center px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route and 'guest' in current_route else '' }}">
<i class="fas fa-user-plus mr-2"></i>
<span>Gast</span>
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin.admin_dashboard') }}"
class="nav-item flex items-center px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route and 'admin' in current_route else '' }}">
<i class="fas fa-cog mr-2"></i>
<span>Admin</span>
</a>
{% endif %}
</div>
{% endif %}
<!-- Right side actions -->
<div class="flex items-center space-x-2">
{% if current_user.is_authenticated %}
<!-- Notifications -->
<div class="relative">
<button id="notificationToggle" class="p-1.5 rounded-lg hover:bg-white/10 dark:hover:bg-black/10 relative">
<i class="fas fa-bell"></i>
<span id="notificationBadge" class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full hidden"></span>
</button>
<!-- Notification Dropdown -->
<div id="notificationDropdown" class="hidden absolute right-0 mt-2 w-80 glass rounded-xl overflow-hidden z-50">
<div class="p-4 border-b border-white/10">
<div class="flex items-center justify-between">
<h3 class="font-semibold">Benachrichtigungen</h3>
<button id="markAllRead" class="text-xs text-blue-500 hover:text-blue-400">
Alle als gelesen markieren
</button>
</div>
</div>
<div id="notificationList" class="max-h-96 overflow-y-auto">
<div class="p-4 text-center text-slate-500 dark:text-slate-400">
<i class="fas fa-bell-slash text-2xl mb-2"></i>
<p>Keine neuen Benachrichtigungen</p>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Dark Mode Toggle -->
<button id="darkModeToggle"
class="p-1.5 rounded-lg hover:bg-white/10 dark:hover:bg-black/10">
<i class="fas fa-sun sun-icon"></i>
<i class="fas fa-moon moon-icon hidden"></i>
</button>
<!-- User Menu -->
{% if current_user.is_authenticated %}
<div class="relative">
<button id="user-menu-btn"
class="flex items-center space-x-2 p-1.5 rounded-lg hover:bg-white/10 dark:hover:bg-black/10">
<div class="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-xs font-bold">
{{ current_user.email[0].upper() if current_user.email else 'U' }}
</div>
<i class="fas fa-chevron-down text-xs"></i>
</button>
<!-- Dropdown -->
<div id="user-dropdown"
class="hidden absolute right-0 mt-2 w-64 glass rounded-xl overflow-hidden">
<div class="p-4 border-b border-white/10">
<p class="font-semibold">{{ current_user.email }}</p>
<p class="text-sm text-slate-600 dark:text-slate-400">{{ 'Administrator' if current_user.is_admin else 'Benutzer' }}</p>
</div>
<div class="p-2">
<a href="{{ url_for('users.user_profile') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10">
<i class="fas fa-user w-4 mr-3"></i>
Mein Profil
</a>
<a href="{{ url_for('users.user_settings') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10">
<i class="fas fa-cog w-4 mr-3"></i>
Einstellungen
</a>
<a href="{{ url_for('guest.guest_requests_overview') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10">
<i class="fas fa-clipboard-list w-4 mr-3"></i>
Meine Anfragen
</a>
<div class="border-t border-white/10 my-2"></div>
<a href="{{ url_for('legal.system_info') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10">
<i class="fas fa-info-circle w-4 mr-3"></i>
System-Info
</a>
<div class="border-t border-white/10 my-2"></div>
<button onclick="handleLogout()"
class="w-full flex items-center px-3 py-2 rounded-lg hover:bg-red-500/20 text-red-600 dark:text-red-400">
<i class="fas fa-sign-out-alt w-4 mr-3"></i>
Abmelden
</button>
</div>
</div>
</div>
{% else %}
<a href="{{ url_for('auth.login') }}"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium">
Anmelden
</a>
{% endif %}
</div>
</div>
</div>
</nav>
<!-- Mobile Menu Overlay -->
<div id="mobile-menu-overlay" class="lg:hidden fixed inset-0 bg-black/50 z-40 hidden"></div>
<!-- Mobile Menu -->
<nav id="mobile-menu" class="lg:hidden fixed top-0 left-0 bottom-0 w-72 glass z-50 mobile-menu">
<div class="p-4">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-bold">Navigation</h2>
<button id="mobile-menu-close" class="p-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10">
<i class="fas fa-times"></i>
</button>
</div>
{% if current_user.is_authenticated %}
<div class="space-y-1">
<a href="{{ url_for('dashboard') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'dashboard' else '' }}">
<i class="fas fa-tachometer-alt w-5 mr-3"></i>
Dashboard
</a>
<a href="{{ url_for('printers_page') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'printers_page' else '' }}">
<i class="fas fa-print w-5 mr-3"></i>
Drucker
</a>
<a href="{{ url_for('jobs_page') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'jobs_page' else '' }}">
<i class="fas fa-tasks w-5 mr-3"></i>
Aufträge
</a>
<a href="{{ url_for('calendar.calendar_view') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'calendar.calendar_view' else '' }}">
<i class="fas fa-calendar w-5 mr-3"></i>
Kalender
</a>
<a href="{{ url_for('energy.energy_dashboard') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'energy.energy_dashboard' else '' }}">
<i class="fas fa-bolt w-5 mr-3"></i>
Energie
</a>
<a href="{{ url_for('stats_page') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route == 'stats_page' else '' }}">
<i class="fas fa-chart-bar w-5 mr-3"></i>
Statistiken
</a>
<a href="{{ url_for('guest.guest_request_form') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route and 'guest' in current_route else '' }}">
<i class="fas fa-user-plus w-5 mr-3"></i>
Gast-Anfrage
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin.admin_dashboard') }}"
class="flex items-center px-3 py-2 rounded-lg hover:bg-white/10 dark:hover:bg-black/10 {{ 'nav-active' if current_route and 'admin' in current_route else '' }}">
<i class="fas fa-cog w-5 mr-3"></i>
Admin
</a>
{% endif %}
</div>
{% endif %}
</div>
</nav>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="fixed top-20 right-4 z-50 space-y-2">
{% for category, message in messages %}
<div class="glass rounded-lg p-4 max-w-sm animate-pulse" id="flash-{{ loop.index }}">
<div class="flex items-start">
<i class="fas {{ 'fa-check-circle text-green-500' if category == 'success' else 'fa-exclamation-circle text-red-500' if category == 'error' else 'fa-info-circle text-blue-500' }} mt-0.5 mr-3"></i>
<div class="flex-1">
<p class="text-sm font-medium">{{ message }}</p>
</div>
<button onclick="this.parentElement.parentElement.remove()" class="ml-3 hover:opacity-70">
<i class="fas fa-times text-sm"></i>
</button>
</div>
</div>
<script>
setTimeout(() => {
document.getElementById('flash-{{ loop.index }}')?.remove();
}, 5000);
</script>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Main Content -->
<main class="main-offset min-h-screen">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{% block content %}{% endblock %}
</div>
</main>
<!-- Footer -->
<footer class="glass border-t border-white/10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Brand -->
<div>
<div class="flex items-center space-x-3 mb-4">
<div class="w-8 h-8">
<svg class="w-full h-full text-slate-900 dark:text-white" fill="currentColor" viewBox="0 0 80 80">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
</div>
<div>
<h3 class="font-bold">MYP</h3>
<p class="text-xs text-slate-600 dark:text-slate-400">Mercedes-Benz</p>
</div>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400">
3D-Druck Management
</p>
</div>
<!-- Quick Links -->
<div>
<h4 class="font-semibold mb-4">Quick Links</h4>
<ul class="space-y-2 text-sm">
<li><a href="{{ url_for('dashboard') }}" class="hover:text-blue-500">Dashboard</a></li>
<li><a href="{{ url_for('printers_page') }}" class="hover:text-blue-500">Drucker</a></li>
<li><a href="{{ url_for('jobs_page') }}" class="hover:text-blue-500">Aufträge</a></li>
<li><a href="{{ url_for('calendar.calendar_view') }}" class="hover:text-blue-500">Kalender</a></li>
</ul>
</div>
<!-- System -->
<div>
<h4 class="font-semibold mb-4">System</h4>
<ul class="space-y-2 text-sm">
<li class="flex items-center">
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
<span>Online</span>
</li>
<li>Version 3.0.0</li>
<li><a href="{{ url_for('legal.system_info') }}" class="hover:text-blue-500">System-Info</a></li>
</ul>
</div>
<!-- Legal -->
<div>
<h4 class="font-semibold mb-4">Rechtliches</h4>
<ul class="space-y-2 text-sm">
<li><a href="{{ url_for('legal.imprint') }}" class="hover:text-blue-500">Impressum</a></li>
<li><a href="{{ url_for('legal.privacy') }}" class="hover:text-blue-500">Datenschutz</a></li>
<li><a href="{{ url_for('legal.terms') }}" class="hover:text-blue-500">Nutzungsbedingungen</a></li>
</ul>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-4">
© 2024 Mercedes-Benz Group AG
</p>
</div>
</div>
</div>
</footer>
<!-- JavaScript -->
<script>
// Dark Mode Toggle - Vereinfachte Version ohne Konflikte
const darkModeToggle = document.getElementById('darkModeToggle');
const sunIcon = document.querySelector('.sun-icon');
const moonIcon = document.querySelector('.moon-icon');
const STORAGE_KEY = 'myp-dark-mode';
function updateDarkModeIcons() {
const isDark = document.documentElement.classList.contains('dark');
if (sunIcon && moonIcon) {
sunIcon.classList.toggle('hidden', isDark);
moonIcon.classList.toggle('hidden', !isDark);
}
// Meta theme color aktualisieren
const themeColorMeta = document.getElementById('theme-color');
if (themeColorMeta) {
themeColorMeta.setAttribute('content', isDark ? '#1e293b' : '#ffffff');
}
}
function setDarkMode(isDark) {
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
localStorage.setItem(STORAGE_KEY, isDark.toString());
updateDarkModeIcons();
// Custom Event für andere Komponenten
window.dispatchEvent(new CustomEvent('darkModeChanged', {
detail: { isDark: isDark }
}));
console.log(`🎨 Theme gewechselt zu: ${isDark ? 'Dark Mode' : 'Light Mode'}`);
}
// Toggle Event Listener
darkModeToggle?.addEventListener('click', () => {
const currentIsDark = document.documentElement.classList.contains('dark');
setDarkMode(!currentIsDark);
});
// Initial setup
updateDarkModeIcons();
// Notification System
class NotificationManager {
constructor() {
this.notifications = [];
this.isOpen = false;
this.initializeElements();
this.setupEventListeners();
this.loadNotifications();
// Auto-refresh alle 30 Sekunden
setInterval(() => this.loadNotifications(), 30000);
}
initializeElements() {
this.toggle = document.getElementById('notificationToggle');
this.dropdown = document.getElementById('notificationDropdown');
this.badge = document.getElementById('notificationBadge');
this.list = document.getElementById('notificationList');
this.markAllRead = document.getElementById('markAllRead');
}
setupEventListeners() {
this.toggle?.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleDropdown();
});
this.markAllRead?.addEventListener('click', () => {
this.markAllAsRead();
});
// Schließen bei Klick außerhalb
document.addEventListener('click', (e) => {
if (!this.dropdown?.contains(e.target) && !this.toggle?.contains(e.target)) {
this.closeDropdown();
}
});
}
async loadNotifications() {
try {
const response = await fetch('/api/notifications');
const data = await response.json();
if (data.success) {
this.notifications = data.notifications || [];
this.updateUI();
}
} catch (error) {
console.error('Fehler beim Laden der Benachrichtigungen:', error);
}
}
updateUI() {
this.updateBadge();
this.updateList();
}
updateBadge() {
const unreadCount = this.notifications.filter(n => !n.is_read).length;
if (this.badge) {
this.badge.classList.toggle('hidden', unreadCount === 0);
}
}
updateList() {
if (!this.list) return;
if (this.notifications.length === 0) {
this.list.innerHTML = `
<div class="p-4 text-center text-slate-500 dark:text-slate-400">
<i class="fas fa-bell-slash text-2xl mb-2"></i>
<p>Keine neuen Benachrichtigungen</p>
</div>
`;
return;
}
const notificationHTML = this.notifications.map(notification => {
const isUnread = !notification.is_read;
const timeAgo = this.formatTimeAgo(new Date(notification.created_at));
return `
<div class="notification-item p-4 border-b border-slate-200 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors ${isUnread ? 'bg-blue-50 dark:bg-blue-900/20' : ''}"
data-notification-id="${notification.id}">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
${this.getNotificationIcon(notification.type)}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-slate-900 dark:text-white">
${notification.title || this.getNotificationTitle(notification.type)}
</p>
${isUnread ? '<div class="w-2 h-2 bg-blue-500 rounded-full"></div>' : ''}
</div>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
${notification.message || this.getNotificationMessage(notification)}
</p>
<p class="text-xs text-slate-500 dark:text-slate-500 mt-2">
${timeAgo}
</p>
${this.getNotificationActions(notification)}
</div>
</div>
</div>
`;
}).join('');
this.list.innerHTML = notificationHTML;
// Event Listeners für Aktionen hinzufügen
this.setupNotificationActions();
}
getNotificationIcon(type) {
const icons = {
'guest_request': '<i class="fas fa-user-plus text-blue-500"></i>',
'job_completed': '<i class="fas fa-check-circle text-green-500"></i>',
'job_failed': '<i class="fas fa-exclamation-triangle text-red-500"></i>',
'system': '<i class="fas fa-cog text-gray-500"></i>'
};
return icons[type] || '<i class="fas fa-bell text-blue-500"></i>';
}
getNotificationTitle(type) {
const titles = {
'guest_request': 'Neue Gastanfrage',
'job_completed': 'Job abgeschlossen',
'job_failed': 'Job fehlgeschlagen',
'system': 'System-Benachrichtigung'
};
return titles[type] || 'Benachrichtigung';
}
getNotificationMessage(notification) {
if (notification.message) return notification.message;
try {
const payload = JSON.parse(notification.payload || '{}');
if (notification.type === 'guest_request') {
return `Gastanfrage von ${payload.name || 'Unbekannt'} wartet auf Genehmigung.`;
}
} catch (e) {
console.warn('Fehler beim Parsen der Notification-Payload:', e);
}
return 'Neue Benachrichtigung verfügbar.';
}
getNotificationActions(notification) {
if (notification.type === 'guest_request') {
try {
const payload = JSON.parse(notification.payload || '{}');
return `
<div class="mt-3 flex space-x-2">
<button onclick="window.notificationManager.viewGuestRequest(${payload.request_id})"
class="px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600">
Anzeigen
</button>
<button onclick="window.notificationManager.markAsRead(${notification.id})"
class="px-3 py-1 bg-gray-500 text-white text-xs rounded hover:bg-gray-600">
Als gelesen markieren
</button>
</div>
`;
} catch (e) {
return '';
}
}
return '';
}
setupNotificationActions() {
// Event Listeners werden über onclick direkt gesetzt
}
async viewGuestRequest(requestId) {
// Weiterleitung zur Admin-Gastanfragen-Seite
window.location.href = `/admin/guest-requests?highlight=${requestId}`;
}
async markAsRead(notificationId) {
try {
const response = await fetch(`/api/notifications/${notificationId}/read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
// Benachrichtigung als gelesen markieren
const notification = this.notifications.find(n => n.id === notificationId);
if (notification) {
notification.is_read = true;
}
this.updateUI();
}
} catch (error) {
console.error('Fehler beim Markieren als gelesen:', error);
}
}
async markAllAsRead() {
try {
const response = await fetch('/api/notifications/mark-all-read', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
// Alle Benachrichtigungen als gelesen markieren
this.notifications.forEach(n => n.is_read = true);
this.updateUI();
}
} catch (error) {
console.error('Fehler beim Markieren aller als gelesen:', error);
}
}
toggleDropdown() {
if (this.dropdown) {
this.isOpen = !this.isOpen;
this.dropdown.classList.toggle('hidden', !this.isOpen);
if (this.isOpen) {
this.loadNotifications(); // Aktualisieren beim Öffnen
}
}
}
closeDropdown() {
if (this.dropdown && this.isOpen) {
this.isOpen = false;
this.dropdown.classList.add('hidden');
}
}
formatTimeAgo(date) {
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) return 'Gerade eben';
if (diffInSeconds < 3600) return `vor ${Math.floor(diffInSeconds / 60)} Min.`;
if (diffInSeconds < 86400) return `vor ${Math.floor(diffInSeconds / 3600)} Std.`;
return `vor ${Math.floor(diffInSeconds / 86400)} Tag(en)`;
}
}
// Mobile Menu
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const mobileMenuClose = document.getElementById('mobile-menu-close');
const mobileMenu = document.getElementById('mobile-menu');
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
function openMobileMenu() {
mobileMenu?.classList.add('active');
mobileMenuOverlay?.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function closeMobileMenu() {
mobileMenu?.classList.remove('active');
mobileMenuOverlay?.classList.add('hidden');
document.body.style.overflow = '';
}
mobileMenuBtn?.addEventListener('click', openMobileMenu);
mobileMenuClose?.addEventListener('click', closeMobileMenu);
mobileMenuOverlay?.addEventListener('click', closeMobileMenu);
// User Dropdown
const userMenuBtn = document.getElementById('user-menu-btn');
const userDropdown = document.getElementById('user-dropdown');
userMenuBtn?.addEventListener('click', (e) => {
e.stopPropagation();
userDropdown?.classList.toggle('hidden');
});
document.addEventListener('click', () => {
userDropdown?.classList.add('hidden');
});
userDropdown?.addEventListener('click', (e) => {
e.stopPropagation();
});
// Logout Handler
function handleLogout() {
if (confirm('Möchten Sie sich wirklich abmelden?')) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("auth.logout") }}';
const csrfToken = document.querySelector('meta[name="csrf-token"]');
if (csrfToken) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'csrf_token';
input.value = csrfToken.getAttribute('content');
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
}
}
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
// Initialize Notification Manager
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('notificationToggle')) {
window.notificationManager = new NotificationManager();
}
});
</script>
<!-- HTMX Non-Invasive Integration (lädt nur bei Bedarf) -->
<script>
// Nur laden wenn HTMX-Attribute gefunden werden
if (document.querySelector('[hx-get], [hx-post], [data-htmx-trigger]')) {
const script = document.createElement('script');
script.src = '{{ url_for("static", filename="js/htmx-integration.js") }}';
script.async = true;
document.head.appendChild(script);
}
</script>
<!-- Jobs Safety Fix laden -->
<script src="{{ url_for('static', filename='js/jobs-safety-fix.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>