Files
Projektarbeit-MYP/backend/templates/base.html

907 lines
42 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>
<form method="POST" action="{{ url_for('auth.logout') }}" class="w-full">
{{ csrf_token() }}
<button type="submit" 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>
</form>
</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 type="button" class="ml-3 hover:opacity-70 close-flash-btn">
<i class="fas fa-times text-sm"></i>
</button>
</div>
</div>
{% 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">
<a href="/admin/guest-requests?highlight=${payload.request_id}"
class="inline-block px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600">
Anzeigen
</a>
<button data-notification-id="${notification.id}" class="mark-read-btn 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
}
// Replaced by direct links in templates
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();
});
// Flash-Message close buttons
document.querySelectorAll('.close-flash-btn').forEach(btn => {
btn.addEventListener('click', function() {
this.parentElement.parentElement.remove();
});
});
// Auto-hide flash messages after 5 seconds
document.querySelectorAll('[id^="flash-"]').forEach((flash, index) => {
setTimeout(() => {
flash?.remove();
}, 5000 + (index * 500)); // Staggered removal
});
// 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>