Projektarbeit-MYP/backend/static/js/ui-components.js
2025-06-01 01:09:49 +02:00

1960 lines
77 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* MYP Platform UI Components
* JavaScript-Utilities für erweiterte UI-Funktionalität
* Version: 3.0.0
*/
(function() {
'use strict';
// Namespace für MYP UI Components
window.MYP = window.MYP || {};
window.MYP.UI = window.MYP.UI || {};
/**
* Dark Mode Handler
*/
class DarkModeManager {
constructor() {
// DOM-Elemente
this.darkModeToggle = document.getElementById('darkModeToggle');
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
this.STORAGE_KEY = 'myp-dark-mode';
this.init();
}
/**
* Aktuellen Dark Mode Status aus Local Storage oder Systemeinstellung abrufen
* @returns {boolean} True wenn Dark Mode aktiviert ist
*/
isDarkMode() {
const savedMode = localStorage.getItem(this.STORAGE_KEY);
if (savedMode !== null) {
return savedMode === 'true';
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
/**
* Dark Mode aktivieren/deaktivieren
* @param {boolean} enable - Dark Mode aktivieren (true) oder deaktivieren (false)
*/
setDarkMode(enable) {
if (enable) {
this.html.classList.add('dark');
this.html.setAttribute('data-theme', 'dark');
this.html.style.colorScheme = 'dark';
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');
this.html.style.colorScheme = 'light';
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
localStorage.setItem(this.STORAGE_KEY, enable);
// 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 }
}));
console.log(`${enable ? '🌙' : '☀️'} Design umgeschaltet auf: ${enable ? 'Dark Mode' : 'Light Mode'}`);
}
/**
* Icon für Dark Mode Toggle aktualisieren
* @param {boolean} isDark - Ob Dark Mode aktiv ist
*/
updateDarkModeIcon(isDark) {
if (!this.darkModeToggle) return;
// 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');
}
}
}
/**
* Event Listener einrichten und Darkmode initialisieren
*/
init() {
if (!this.darkModeToggle) {
console.error('⚠️ Dark Mode Toggle Button nicht gefunden! UI-Komponente nicht verfügbar.');
return;
}
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 Button angeklickt');
const newDarkModeState = !this.isDarkMode();
this.setDarkMode(newDarkModeState);
});
// 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 aktiviert');
const newDarkModeState = !this.isDarkMode();
this.setDarkMode(newDarkModeState);
}
});
// Tastaturkürzel: Strg+Shift+D für Dark Mode Toggle
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
const newDarkModeState = !this.isDarkMode();
this.setDarkMode(newDarkModeState);
e.preventDefault();
}
});
// System-Preference für Dark Mode überwachen
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeMediaQuery.addEventListener('change', (e) => {
// Nur ändern, wenn der Benutzer noch keine eigene Einstellung hat
if (localStorage.getItem(this.STORAGE_KEY) === null) {
this.setDarkMode(e.matches);
}
});
// Initialisierung: Aktuellen Status setzen
const isDark = this.isDarkMode();
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
*/
class UserDropdownManager {
constructor() {
this.toggleButton = document.getElementById('user-menu-button');
this.container = document.getElementById('user-menu-container');
this.dropdown = document.getElementById('user-dropdown');
this.isOpen = false;
// Dropdown erstellen, falls nicht vorhanden
if (!this.dropdown && this.container) {
this.createDropdown();
}
this.init();
}
/**
* Dropdown-Menü dynamisch erstellen, falls es nicht existiert
*/
createDropdown() {
if (!this.container) return;
// Neue Dropdown-Box erstellen
this.dropdown = document.createElement('div');
this.dropdown.id = 'user-dropdown';
this.dropdown.className = 'user-dropdown hidden';
this.dropdown.setAttribute('role', 'menu');
this.dropdown.setAttribute('aria-orientation', 'vertical');
this.dropdown.setAttribute('aria-labelledby', 'user-menu-button');
// HTML für das Dropdown-Menü
this.dropdown.innerHTML = `
<div class="dropdown-header">
<div class="avatar-large">
${this.getCurrentUserInitial()}
</div>
<div class="ml-3">
<p class="text-sm font-medium text-slate-900 dark:text-white transition-colors duration-300">
${this.getCurrentUsername()}
</p>
<p class="text-xs text-slate-500 dark:text-slate-400 transition-colors duration-300">
${this.getCurrentUserEmail()}
</p>
</div>
</div>
<div class="dropdown-divider"></div>
<a href="/profil" id="profile-link" class="dropdown-item" role="menuitem">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
<span>Mein Profil</span>
</a>
<a href="/einstellungen" id="settings-link" class="dropdown-item" role="menuitem">
<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>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>Einstellungen</span>
</a>
<div class="dropdown-divider"></div>
<button type="button" data-action="logout" class="dropdown-item text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/10" role="menuitem">
<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 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
</svg>
<span>Abmelden</span>
</button>
`;
// Dropdown zum Container hinzufügen
this.container.appendChild(this.dropdown);
}
/**
* Benutzername aus E-Mail extrahieren oder Standardwert verwenden
*/
getCurrentUsername() {
const metaUsername = document.querySelector('meta[name="user-name"]');
if (metaUsername) {
return metaUsername.getAttribute('content');
}
// Aus dem vorhandenen DOM extrahieren
const usernameElement = this.container.querySelector('.text-sm.font-medium');
if (usernameElement) {
return usernameElement.textContent.trim();
}
return 'Benutzer';
}
/**
* E-Mail-Adresse aus dem DOM extrahieren
*/
getCurrentUserEmail() {
const metaEmail = document.querySelector('meta[name="user-email"]');
if (metaEmail) {
return metaEmail.getAttribute('content');
}
// Aus dem vorhandenen DOM extrahieren
const emailElement = this.container.querySelector('.text-xs');
if (emailElement) {
return emailElement.textContent.trim();
}
return 'Mercedes-Benz Mitarbeiter';
}
/**
* Initial des Benutzers extrahieren
*/
getCurrentUserInitial() {
const avatar = this.container.querySelector('.user-avatar');
if (avatar) {
return avatar.textContent.trim();
}
return 'U';
}
open() {
if (!this.dropdown || this.isOpen) return;
this.dropdown.classList.remove('hidden');
this.isOpen = true;
this.toggleButton.setAttribute('aria-expanded', 'true');
// Rotate dropdown arrow
const arrow = this.toggleButton.querySelector('svg');
if (arrow) {
arrow.style.transform = 'rotate(180deg)';
}
// Close when clicking outside
setTimeout(() => {
document.addEventListener('click', this.outsideClickHandler);
}, 10);
// Close when pressing escape
document.addEventListener('keydown', this.escapeKeyHandler);
}
close() {
if (!this.dropdown || !this.isOpen) return;
this.dropdown.classList.add('hidden');
this.isOpen = false;
this.toggleButton.setAttribute('aria-expanded', 'false');
// Reset dropdown arrow
const arrow = this.toggleButton.querySelector('svg');
if (arrow) {
arrow.style.transform = 'rotate(0deg)';
}
// Remove event listeners
document.removeEventListener('click', this.outsideClickHandler);
document.removeEventListener('keydown', this.escapeKeyHandler);
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
init() {
if (!this.toggleButton || !this.dropdown) return;
// Bind methods to this instance
this.outsideClickHandler = this.handleOutsideClick.bind(this);
this.escapeKeyHandler = this.handleEscapeKey.bind(this);
// Set up click listener for toggle button
this.toggleButton.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// Set up logout button
const logoutButton = this.dropdown.querySelector('[data-action="logout"]');
if (logoutButton) {
logoutButton.addEventListener('click', this.handleLogout.bind(this));
}
// Set up settings button
const settingsLink = this.dropdown.querySelector('#settings-link');
if (settingsLink) {
settingsLink.addEventListener('click', this.handleSettings.bind(this));
}
}
handleOutsideClick(e) {
if (this.container && !this.container.contains(e.target)) {
this.close();
}
}
handleEscapeKey(e) {
if (e.key === 'Escape') {
this.close();
}
}
handleLogout(e) {
e.preventDefault();
// Add smooth logout animation
document.body.style.opacity = '0.7';
// Create and submit logout form
const form = document.createElement('form');
form.method = 'POST';
form.action = '/logout';
// Add CSRF token if available
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);
}
// Submit the form
document.body.appendChild(form);
form.submit();
}
handleSettings(e) {
// Optionales preventDefault für spezielle Handhabung
// e.preventDefault();
}
}
/**
* Toast-Benachrichtigungen
*/
class ToastManager {
constructor() {
this.container = this.createContainer();
this.toasts = new Map();
}
createContainer() {
let container = document.getElementById('toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'toast-container';
container.className = 'fixed top-4 right-4 z-50 space-y-2';
document.body.appendChild(container);
}
return container;
}
show(message, type = 'info', duration = 5000) {
const id = 'toast-' + Date.now();
const toast = this.createToast(id, message, type);
this.container.appendChild(toast);
this.toasts.set(id, toast);
// Animation einblenden
requestAnimationFrame(() => {
toast.classList.remove('translate-x-full', 'opacity-0');
});
// Auto-Hide nach duration
if (duration > 0) {
setTimeout(() => this.hide(id), duration);
}
return id;
}
createToast(id, message, type) {
const toast = document.createElement('div');
toast.id = id;
toast.className = `transform translate-x-full opacity-0 transition-all duration-300 ease-in-out max-w-sm w-full bg-white dark:bg-slate-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden`;
const typeClasses = {
'success': 'border-l-4 border-green-400',
'error': 'border-l-4 border-red-400',
'warning': 'border-l-4 border-yellow-400',
'info': 'border-l-4 border-blue-400'
};
const iconHTML = {
'success': '<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>',
'error': '<svg class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>',
'warning': '<svg class="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>',
'info': '<svg class="w-5 h-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>'
};
toast.className += ' ' + (typeClasses[type] || typeClasses.info);
toast.innerHTML = `
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
${iconHTML[type] || iconHTML.info}
</div>
<div class="ml-3 w-0 flex-1">
<p class="text-sm font-medium text-gray-900 dark:text-white">
${message}
</p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button onclick="window.MYP.UI.toast.hide('${id}')"
class="bg-white dark:bg-slate-800 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span class="sr-only">Schließen</span>
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
</div>
</div>
</div>
`;
return toast;
}
hide(id) {
const toast = this.toasts.get(id);
if (toast) {
toast.classList.add('translate-x-full', 'opacity-0');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
this.toasts.delete(id);
}, 300);
}
}
success(message, duration) {
return this.show(message, 'success', duration);
}
error(message, duration) {
return this.show(message, 'error', duration);
}
warning(message, duration) {
return this.show(message, 'warning', duration);
}
info(message, duration) {
return this.show(message, 'info', duration);
}
}
/**
* Modal-Dialog-Manager
*/
class ModalManager {
constructor() {
this.activeModals = new Set();
this.setupEventListeners();
}
setupEventListeners() {
// ESC-Taste zum Schließen
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.activeModals.size > 0) {
this.closeTopModal();
}
});
}
open(modalId, options = {}) {
const modal = document.getElementById(modalId);
if (!modal) {
console.error(`Modal mit ID '${modalId}' nicht gefunden`);
return;
}
// Backdrop erstellen
const backdrop = this.createBackdrop(modalId);
document.body.appendChild(backdrop);
// Modal anzeigen
modal.style.display = 'block';
this.activeModals.add(modalId);
// Animation
requestAnimationFrame(() => {
backdrop.classList.remove('opacity-0');
modal.querySelector('.modal-content')?.classList.remove('scale-95', 'opacity-0');
});
// Body-Scroll verhindern
document.body.style.overflow = 'hidden';
// Auto-Focus auf erstes Input-Element
const firstInput = modal.querySelector('input, select, textarea, button');
if (firstInput) {
firstInput.focus();
}
}
close(modalId) {
const modal = document.getElementById(modalId);
const backdrop = document.getElementById(`backdrop-${modalId}`);
if (modal && backdrop) {
// Animation ausblenden
backdrop.classList.add('opacity-0');
modal.querySelector('.modal-content')?.classList.add('scale-95', 'opacity-0');
setTimeout(() => {
modal.style.display = 'none';
if (backdrop.parentNode) {
backdrop.parentNode.removeChild(backdrop);
}
this.activeModals.delete(modalId);
// Body-Scroll wiederherstellen, falls kein Modal mehr offen
if (this.activeModals.size === 0) {
document.body.style.overflow = '';
}
}, 150);
}
}
closeTopModal() {
if (this.activeModals.size > 0) {
const lastModal = Array.from(this.activeModals).pop();
this.close(lastModal);
}
}
createBackdrop(modalId) {
const backdrop = document.createElement('div');
backdrop.id = `backdrop-${modalId}`;
backdrop.className = 'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity opacity-0 z-40';
backdrop.onclick = () => this.close(modalId);
return backdrop;
}
}
/**
* Dropdown-Manager
*/
class DropdownManager {
constructor() {
this.activeDropdowns = new Set();
this.setupEventListeners();
}
setupEventListeners() {
document.addEventListener('click', (e) => {
// Schließe alle Dropdowns, wenn außerhalb geklickt wird
if (!e.target.closest('[data-dropdown]')) {
this.closeAll();
}
});
}
toggle(dropdownId) {
const dropdown = document.querySelector(`[data-dropdown="${dropdownId}"]`);
if (!dropdown) return;
const menu = dropdown.querySelector('.dropdown-menu');
if (!menu) return;
if (this.activeDropdowns.has(dropdownId)) {
this.close(dropdownId);
} else {
this.closeAll(); // Andere schließen
this.open(dropdownId);
}
}
open(dropdownId) {
const dropdown = document.querySelector(`[data-dropdown="${dropdownId}"]`);
const menu = dropdown?.querySelector('.dropdown-menu');
if (dropdown && menu) {
menu.classList.remove('hidden');
this.activeDropdowns.add(dropdownId);
// Position berechnen
this.positionDropdown(dropdown, menu);
}
}
close(dropdownId) {
const dropdown = document.querySelector(`[data-dropdown="${dropdownId}"]`);
const menu = dropdown?.querySelector('.dropdown-menu');
if (menu) {
menu.classList.add('hidden');
this.activeDropdowns.delete(dropdownId);
}
}
closeAll() {
this.activeDropdowns.forEach(id => this.close(id));
}
positionDropdown(dropdown, menu) {
const rect = dropdown.getBoundingClientRect();
const menuRect = menu.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// Prüfen ob Platz nach unten ist
if (rect.bottom + menuRect.height > viewportHeight) {
// Nach oben öffnen
menu.style.bottom = '100%';
menu.style.top = 'auto';
} else {
// Nach unten öffnen
menu.style.top = '100%';
menu.style.bottom = 'auto';
}
}
}
/**
* Loading-Spinner-Manager
*/
class LoadingManager {
show(target = document.body, message = 'Laden...') {
const loadingId = 'loading-' + Date.now();
const overlay = document.createElement('div');
overlay.id = loadingId;
overlay.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
overlay.innerHTML = `
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 flex items-center space-x-3 shadow-lg">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-indigo-600"></div>
<span class="text-slate-900 dark:text-white font-medium">${message}</span>
</div>
`;
target.appendChild(overlay);
return loadingId;
}
hide(loadingId) {
const overlay = document.getElementById(loadingId);
if (overlay) {
overlay.remove();
}
}
}
/**
* Status-Badge-Helper
*/
class StatusHelper {
static getPrinterStatusClass(status) {
const statusMap = {
'ready': 'printer-ready',
'busy': 'printer-busy',
'error': 'printer-error',
'offline': 'printer-offline',
'maintenance': 'printer-maintenance'
};
return `printer-status ${statusMap[status] || 'printer-offline'}`;
}
static getJobStatusClass(status) {
const statusMap = {
'queued': 'job-queued',
'printing': 'job-printing',
'completed': 'job-completed',
'failed': 'job-failed',
'cancelled': 'job-cancelled',
'paused': 'job-paused'
};
return `job-status ${statusMap[status] || 'job-queued'}`;
}
static formatStatus(status, type = 'job') {
const translations = {
job: {
'queued': 'In Warteschlange',
'printing': 'Wird gedruckt',
'completed': 'Abgeschlossen',
'failed': 'Fehlgeschlagen',
'cancelled': 'Abgebrochen',
'paused': 'Pausiert'
},
printer: {
'ready': 'Bereit',
'busy': 'Beschäftigt',
'error': 'Fehler',
'offline': 'Offline',
'maintenance': 'Wartung'
}
};
return translations[type]?.[status] || status;
}
}
/**
* Connection Status Manager
*/
class ConnectionStatusManager {
constructor() {
this.statusElement = document.getElementById('connection-status');
if (this.statusElement) {
this.init();
}
}
updateStatus() {
if (!this.statusElement) return;
if (navigator.onLine) {
this.statusElement.innerHTML = '<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div><span class="text-green-500 dark:text-green-400 font-medium transition-colors duration-300">Online</span>';
} else {
this.statusElement.innerHTML = '<div class="w-2 h-2 bg-red-400 rounded-full animate-pulse"></div><span class="text-red-500 dark:text-red-400 font-medium transition-colors duration-300">Offline</span>';
}
}
init() {
// Initial Update
this.updateStatus();
// Event Listener für Netzwerkänderungen
window.addEventListener('online', () => this.updateStatus());
window.addEventListener('offline', () => this.updateStatus());
}
}
/**
* API-Aufruf Hilfsfunktion für Fetch-Requests
* @param {string} url - API-Endpunkt
* @param {object} options - Fetch-Optionen
* @returns {Promise<object>} API-Antwort als JSON
*/
async function apiCall(url, options = {}) {
// CSRF-Token aus Meta-Tag auslesen
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
// Standard-Headers setzen
const headers = {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
};
// CSRF-Token hinzufügen, wenn vorhanden
if (csrfToken) {
headers['X-CSRFToken'] = csrfToken;
}
// Optionen zusammenführen
const fetchOptions = {
...options,
headers: {
...headers,
...(options.headers || {})
}
};
try {
const response = await fetch(url, fetchOptions);
// Prüfen, ob die Antwort JSON ist
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
// Wenn die Antwort einen Fehler enthält, werfen wir eine Ausnahme
if (!response.ok) {
throw new Error(data.error || `HTTP-Fehler: ${response.status}`);
}
return data;
}
// Für nicht-JSON-Antworten
if (!response.ok) {
throw new Error(`HTTP-Fehler: ${response.status}`);
}
return { success: true };
} catch (error) {
console.error('API-Aufruf fehlgeschlagen:', error);
throw error;
}
}
/**
* Erweiterte Flash-Nachricht anzeigen mit glasigen Effekten
* @param {string} message - Nachrichtentext
* @param {string} type - Nachrichtentyp (success, error, info, warning)
* @param {number} duration - Anzeigedauer in Millisekunden (Standard: 5000)
*/
function showFlashMessage(message, type = 'info', duration = 5000) {
// Unique ID für die Nachricht
const messageId = 'flash-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
// Flash-Message-Element erstellen
const flashElement = document.createElement('div');
flashElement.id = messageId;
flashElement.className = `flash-message ${type}`;
// Icon basierend auf Typ
let icon = '';
switch(type) {
case 'success':
icon = `<svg class="w-5 h-5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>`;
break;
case 'error':
icon = `<svg class="w-5 h-5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>`;
break;
case 'warning':
icon = `<svg class="w-5 h-5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>`;
break;
case 'info':
default:
icon = `<svg class="w-5 h-5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>`;
}
// Inhalt der Flash Message
flashElement.innerHTML = `
<div class="flex items-start">
${icon}
<div class="flex-1 min-w-0">
<p class="font-medium text-sm leading-relaxed">${message}</p>
</div>
<button class="flash-close-btn ml-4 flex-shrink-0 text-current opacity-70 hover:opacity-100 transition-opacity duration-200 focus:outline-none focus:opacity-100"
onclick="closeFlashMessage('${messageId}')"
aria-label="Nachricht schließen">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
`;
// Flash Message zum DOM hinzufügen
document.body.appendChild(flashElement);
// Flash Messages vertikal stapeln
repositionFlashMessages();
// Einblende-Animation starten
requestAnimationFrame(() => {
flashElement.style.transform = 'translateX(0) translateY(0)';
flashElement.style.opacity = '1';
});
// Nach der angegebenen Zeit automatisch entfernen
setTimeout(() => {
closeFlashMessage(messageId);
}, duration);
return messageId;
}
/**
* Flash Message schließen
* @param {string} messageId - ID der zu schließenden Nachricht
*/
function closeFlashMessage(messageId) {
const flashElement = document.getElementById(messageId);
if (flashElement) {
flashElement.classList.add('hiding');
setTimeout(() => {
if (flashElement.parentNode) {
flashElement.parentNode.removeChild(flashElement);
}
repositionFlashMessages();
}, 400); // Dauer der Ausblende-Animation
}
}
/**
* Flash Messages neu positionieren für Stapel-Effekt
*/
function repositionFlashMessages() {
const flashMessages = document.querySelectorAll('.flash-message:not(.hiding)');
const gap = 12; // Abstand zwischen Messages
const startTop = 16; // Top-Offset
flashMessages.forEach((flash, index) => {
const yPosition = startTop + (index * (80 + gap)); // 80px Höhe + gap
// Position setzen
flash.style.position = 'fixed';
flash.style.top = `${yPosition}px`;
flash.style.right = '16px';
flash.style.zIndex = 50 + (flashMessages.length - index); // Neueste Messages haben höheren z-index
flash.style.width = 'auto';
flash.style.maxWidth = '420px';
flash.style.minWidth = '320px';
// Initiale Position für Animation (nur bei neuen Messages)
if (!flash.style.transform) {
flash.style.transform = 'translateX(100%) translateY(-20px)';
flash.style.opacity = '0';
}
});
}
/**
* Navbar Scroll Manager - Glassmorphism Effekte beim Scrollen
*/
class NavbarScrollManager {
constructor() {
this.navbar = document.querySelector('.navbar');
this.isScrolled = false;
this.scrollThreshold = 50; // Ab wie vielen Pixeln der Effekt aktiviert wird
this.ticking = false; // Für requestAnimationFrame Optimierung
this.init();
}
/**
* Scroll-Handler mit Performance-Optimierung
*/
handleScroll() {
if (!this.ticking) {
requestAnimationFrame(() => {
this.updateNavbar();
this.ticking = false;
});
this.ticking = true;
}
}
/**
* Navbar basierend auf Scroll-Position aktualisieren
*/
updateNavbar() {
if (!this.navbar) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldBeScrolled = scrollTop > this.scrollThreshold;
if (shouldBeScrolled !== this.isScrolled) {
this.isScrolled = shouldBeScrolled;
if (this.isScrolled) {
this.navbar.classList.add('scrolled');
this.navbar.style.transform = 'translateY(0)';
} else {
this.navbar.classList.remove('scrolled');
}
// Event für andere Komponenten auslösen
window.dispatchEvent(new CustomEvent('navbarScrolled', {
detail: { isScrolled: this.isScrolled, scrollTop }
}));
}
}
/**
* Sanfte Navbar-Animation beim Scrollen nach oben/unten
*/
handleDirectionalScroll() {
let lastScrollTop = 0;
return () => {
if (!this.navbar) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
if (scrollTop > lastScrollTop && scrollTop > this.scrollThreshold) {
// Scrollen nach unten - Navbar ausblenden
this.navbar.style.transform = 'translateY(-100%)';
} else {
// Scrollen nach oben - Navbar einblenden
this.navbar.style.transform = 'translateY(0)';
}
lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
};
}
/**
* Parallax-Effekt für die Navbar
*/
applyParallaxEffect() {
if (!this.navbar) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const parallaxSpeed = 0.5;
const yPos = -(scrollTop * parallaxSpeed);
// Nur bei größeren Bildschirmen anwenden
if (window.innerWidth > 768) {
this.navbar.style.transform = `translateY(${yPos}px)`;
}
}
/**
* Glassmorphism-Intensität basierend auf Scroll-Position
*/
updateGlassmorphismIntensity() {
if (!this.navbar) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const maxScroll = 200; // Maximale Scroll-Distanz für vollen Effekt
const intensity = Math.min(scrollTop / maxScroll, 1);
// CSS Custom Properties für dynamische Blur-Werte
const blurValue = 40 + (intensity * 20); // Von 40px zu 60px
const opacityValue = 0.15 + (intensity * 0.2); // Von 0.15 zu 0.35
this.navbar.style.setProperty('--navbar-blur', `${blurValue}px`);
this.navbar.style.setProperty('--navbar-opacity', opacityValue);
}
/**
* Navbar-Manager initialisieren
*/
init() {
if (!this.navbar) {
console.log(' Navbar nicht gefunden - Scroll-Effekte deaktiviert');
return;
}
console.log('🌊 Navbar Scroll Manager erfolgreich initialisiert');
// CSS Custom Properties initialisieren
this.navbar.style.setProperty('--navbar-blur', '40px');
this.navbar.style.setProperty('--navbar-opacity', '0.15');
// Event Listener für Scroll-Events
window.addEventListener('scroll', this.handleScroll.bind(this), { passive: true });
// Optionale erweiterte Effekte (können aktiviert werden)
// const directionalScroll = this.handleDirectionalScroll();
// window.addEventListener('scroll', directionalScroll, { passive: true });
// Resize-Handler für responsive Anpassungen
window.addEventListener('resize', () => {
this.updateNavbar();
});
// Initialer Status setzen
this.updateNavbar();
}
}
/**
* Do Not Disturb Manager
* Verwaltet den Do Not Disturb-Modus für Flash Messages und Benachrichtigungen
*/
class DoNotDisturbManager {
constructor() {
this.isActive = false;
this.suppressedMessages = [];
this.settings = {
allowCritical: true,
allowErrorsOnly: false,
suppressDuration: 60, // Minuten
autoDisable: true
};
this.suppressEndTime = null;
this.indicator = null;
this.counter = 0;
this.init();
}
/**
* Do Not Disturb-System initialisieren
*/
init() {
this.loadSettings();
this.createIndicator();
this.setupEventListeners();
this.checkAutoDisable();
console.log('🔕 Do Not Disturb Manager erfolgreich initialisiert');
}
/**
* Einstellungen aus localStorage laden
*/
loadSettings() {
try {
const saved = localStorage.getItem('dnd-settings');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
const state = localStorage.getItem('dnd-state');
if (state) {
const savedState = JSON.parse(state);
this.isActive = savedState.isActive || false;
this.suppressEndTime = savedState.suppressEndTime ? new Date(savedState.suppressEndTime) : null;
this.suppressedMessages = savedState.suppressedMessages || [];
this.counter = savedState.counter || 0;
}
} catch (error) {
console.error('Fehler beim Laden der DND-Einstellungen:', error);
}
}
/**
* Einstellungen in localStorage speichern
*/
saveSettings() {
try {
localStorage.setItem('dnd-settings', JSON.stringify(this.settings));
localStorage.setItem('dnd-state', JSON.stringify({
isActive: this.isActive,
suppressEndTime: this.suppressEndTime,
suppressedMessages: this.suppressedMessages,
counter: this.counter
}));
} catch (error) {
console.error('Fehler beim Speichern der DND-Einstellungen:', error);
}
}
/**
* DND-Indikator erstellen
*/
createIndicator() {
this.indicator = document.createElement('div');
this.indicator.className = 'dnd-indicator';
this.indicator.innerHTML = `
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zM9 8a1 1 0 012 0v4a1 1 0 11-2 0V8z" clip-rule="evenodd"/>
</svg>
<span class="dnd-text">Nicht stören</span>
<span class="dnd-counter-badge ml-2 px-2 py-1 text-xs rounded-full bg-red-500 text-white hidden">0</span>
`;
this.indicator.addEventListener('click', () => this.showSettings());
document.body.appendChild(this.indicator);
this.updateIndicator();
}
/**
* Event-Listener einrichten
*/
setupEventListeners() {
// Original showFlashMessage überschreiben
const originalShowFlashMessage = window.showFlashMessage;
window.showFlashMessage = (message, type = 'info') => {
this.handleFlashMessage(message, type, originalShowFlashMessage);
};
// Original showToast überschreiben
if (window.showToast) {
const originalShowToast = window.showToast;
window.showToast = (message, type = 'info', duration) => {
this.handleToastMessage(message, type, duration, originalShowToast);
};
}
// Periodisch Auto-Disable prüfen
setInterval(() => this.checkAutoDisable(), 60000); // Jede Minute
}
/**
* Flash Message verarbeiten
*/
handleFlashMessage(message, type, originalFunction) {
if (this.shouldSuppressMessage(type)) {
this.addSuppressedMessage(message, type, 'flash');
this.showSuppressedMessage(message, type);
} else {
originalFunction(message, type);
}
}
/**
* Toast Message verarbeiten
*/
handleToastMessage(message, type, duration, originalFunction) {
if (this.shouldSuppressMessage(type)) {
this.addSuppressedMessage(message, type, 'toast');
this.showSuppressedMessage(message, type);
} else {
originalFunction(message, type, duration);
}
}
/**
* Prüfen, ob Nachricht unterdrückt werden soll
*/
shouldSuppressMessage(type) {
if (!this.isActive) return false;
// Kritische Nachrichten immer anzeigen (falls eingestellt)
if (this.settings.allowCritical && (type === 'error' || type === 'critical')) {
return false;
}
// Nur Fehler anzeigen (falls eingestellt)
if (this.settings.allowErrorsOnly && type !== 'error') {
return true;
}
return true;
}
/**
* Unterdrückte Nachricht hinzufügen
*/
addSuppressedMessage(message, type, source) {
const suppressedMessage = {
id: Date.now(),
message,
type,
source,
timestamp: new Date(),
read: false
};
this.suppressedMessages.unshift(suppressedMessage);
this.counter++;
// Nur die letzten 50 Nachrichten behalten
if (this.suppressedMessages.length > 50) {
this.suppressedMessages = this.suppressedMessages.slice(0, 50);
}
this.updateIndicator();
this.saveSettings();
}
/**
* Gedämpfte Version der Nachricht anzeigen
*/
showSuppressedMessage(message, type) {
const suppressedFlash = document.createElement('div');
suppressedFlash.className = `flash-message dnd-suppressed ${type}`;
suppressedFlash.innerHTML = `
<div class="flex items-center">
<svg class="w-4 h-4 mr-2 opacity-50" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zM9 8a1 1 0 012 0v4a1 1 0 11-2 0V8z" clip-rule="evenodd"/>
</svg>
<span class="opacity-70 text-xs">Nachricht unterdrückt</span>
</div>
`;
suppressedFlash.style.top = '4rem';
document.body.appendChild(suppressedFlash);
setTimeout(() => {
suppressedFlash.remove();
}, 2000);
}
/**
* DND-Modus aktivieren
*/
enable(duration = null) {
this.isActive = true;
if (duration) {
this.suppressEndTime = new Date(Date.now() + duration * 60000);
} else {
this.suppressEndTime = null;
}
this.updateIndicator();
this.saveSettings();
console.log('🔕 Do Not Disturb aktiviert', duration ? `für ${duration} Minuten` : 'dauerhaft');
}
/**
* DND-Modus deaktivieren
*/
disable() {
this.isActive = false;
this.suppressEndTime = null;
this.updateIndicator();
this.saveSettings();
console.log('🔔 Do Not Disturb deaktiviert');
}
/**
* DND-Modus umschalten
*/
toggle() {
if (this.isActive) {
this.disable();
} else {
this.showSettings();
}
}
/**
* Auto-Disable prüfen
*/
checkAutoDisable() {
if (this.isActive && this.suppressEndTime && new Date() >= this.suppressEndTime) {
this.disable();
if (window.showToast) {
window.showToast('Do Not Disturb automatisch deaktiviert', 'info');
}
}
}
/**
* Indikator aktualisieren
*/
updateIndicator() {
if (!this.indicator) return;
if (this.isActive) {
this.indicator.classList.add('active');
// Counter Badge aktualisieren
const badge = this.indicator.querySelector('.dnd-counter-badge');
if (this.counter > 0) {
badge.textContent = this.counter > 99 ? '99+' : this.counter;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
// Zeitanzeige
const text = this.indicator.querySelector('.dnd-text');
if (this.suppressEndTime) {
const remaining = Math.ceil((this.suppressEndTime - new Date()) / 60000);
text.textContent = `Nicht stören (${remaining}min)`;
} else {
text.textContent = 'Nicht stören';
}
} else {
this.indicator.classList.remove('active');
}
}
/**
* Einstellungs-Modal anzeigen
*/
showSettings() {
// Prüfen ob Modal bereits offen ist
if (document.querySelector('.dnd-modal')) {
return; // Modal bereits offen, nicht doppelt öffnen
}
const modal = document.createElement('div');
modal.className = 'dnd-modal';
modal.innerHTML = `
<div class="dnd-modal-content">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
🔕 Nicht stören
</h3>
<button class="dnd-close-btn text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors duration-200"
type="button">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="space-y-4">
<!-- Schnell-Aktionen -->
<div class="grid grid-cols-2 gap-3">
<button class="dnd-quick-btn btn-primary" data-duration="30" type="button">
30 Min
</button>
<button class="dnd-quick-btn btn-primary" data-duration="60" type="button">
1 Stunde
</button>
<button class="dnd-quick-btn btn-primary" data-duration="480" type="button">
8 Stunden
</button>
<button class="dnd-quick-btn btn-primary" data-duration="0" type="button">
Dauerhaft
</button>
</div>
<!-- Erweiterte Einstellungen -->
<div class="pt-4 border-t border-gray-200/30 dark:border-slate-700/30 space-y-3">
<label class="flex items-center space-x-3">
<input type="checkbox" class="dnd-setting" data-setting="allowCritical"
${this.settings.allowCritical ? 'checked' : ''}>
<span class="text-sm text-slate-700 dark:text-slate-300">
Kritische Fehler anzeigen
</span>
</label>
<label class="flex items-center space-x-3">
<input type="checkbox" class="dnd-setting" data-setting="allowErrorsOnly"
${this.settings.allowErrorsOnly ? 'checked' : ''}>
<span class="text-sm text-slate-700 dark:text-slate-300">
Nur Fehler anzeigen
</span>
</label>
</div>
<!-- Unterdrückte Nachrichten -->
${this.suppressedMessages.length > 0 ? `
<div class="pt-4 border-t border-gray-200/30 dark:border-slate-700/30">
<div class="flex items-center justify-between mb-3">
<h4 class="text-sm font-medium text-slate-900 dark:text-white">
Unterdrückte Nachrichten (${this.suppressedMessages.length})
</h4>
<button class="dnd-clear-btn text-xs text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors duration-200"
type="button">
Alle löschen
</button>
</div>
<div class="max-h-32 overflow-y-auto space-y-2">
${this.suppressedMessages.slice(0, 5).map(msg => `
<div class="text-xs p-2 rounded bg-slate-100/50 dark:bg-slate-800/50">
<span class="font-medium text-${msg.type === 'error' ? 'red' : msg.type === 'warning' ? 'yellow' : msg.type === 'success' ? 'green' : 'blue'}-600 dark:text-${msg.type === 'error' ? 'red' : msg.type === 'warning' ? 'yellow' : msg.type === 'success' ? 'green' : 'blue'}-400">
${msg.type.toUpperCase()}:
</span>
<span class="text-slate-600 dark:text-slate-400">
${msg.message}
</span>
<div class="text-xs text-slate-400 dark:text-slate-500 mt-1">
${msg.timestamp.toLocaleTimeString()}
</div>
</div>
`).join('')}
${this.suppressedMessages.length > 5 ? `
<div class="text-xs text-center text-slate-500 dark:text-slate-400 py-2">
... und ${this.suppressedMessages.length - 5} weitere
</div>
` : ''}
</div>
</div>
` : ''}
<!-- Aktions-Buttons -->
<div class="flex space-x-3 pt-4">
${this.isActive ? `
<button class="dnd-disable-btn btn-secondary flex-1" type="button">
Deaktivieren
</button>
` : ''}
<button class="dnd-close-btn-main btn-primary flex-1" type="button">
Schließen
</button>
</div>
</div>
</div>
`;
// Event Listeners mit Event Delegation
modal.addEventListener('click', (e) => {
e.stopPropagation();
// Schließen bei Klick auf Hintergrund
if (e.target === modal) {
this.closeModal(modal);
return;
}
// Schließen-Buttons
if (e.target.closest('.dnd-close-btn') || e.target.closest('.dnd-close-btn-main')) {
e.preventDefault();
this.closeModal(modal);
return;
}
// Schnell-Aktionen
if (e.target.classList.contains('dnd-quick-btn')) {
e.preventDefault();
const duration = parseInt(e.target.dataset.duration);
if (duration === 0) {
this.enable();
} else {
this.enable(duration);
}
this.closeModal(modal);
return;
}
// Deaktivieren-Button
if (e.target.classList.contains('dnd-disable-btn')) {
e.preventDefault();
this.disable();
this.closeModal(modal);
return;
}
// Alle löschen
if (e.target.classList.contains('dnd-clear-btn')) {
e.preventDefault();
this.clearSuppressedMessages();
this.closeModal(modal);
// Modal neu öffnen mit aktualisierten Daten
setTimeout(() => this.showSettings(), 100);
return;
}
// Einstellungen-Checkboxen
if (e.target.classList.contains('dnd-setting')) {
const setting = e.target.dataset.setting;
this.settings[setting] = e.target.checked;
this.saveSettings();
return;
}
});
// ESC-Taste zum Schließen
const escapeHandler = (e) => {
if (e.key === 'Escape') {
this.closeModal(modal);
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
// Modal zum DOM hinzufügen
document.body.appendChild(modal);
}
/**
* Modal schließen
*/
closeModal(modal) {
if (modal && modal.parentNode) {
modal.style.opacity = '0';
modal.style.pointerEvents = 'none';
setTimeout(() => {
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
}, 200);
}
}
/**
* Unterdrückte Nachrichten löschen
*/
clearSuppressedMessages() {
this.suppressedMessages = [];
this.counter = 0;
this.updateIndicator();
this.saveSettings();
}
/**
* Unterdrückte Nachrichten abrufen
*/
getSuppressedMessages() {
return [...this.suppressedMessages];
}
/**
* Status abrufen
*/
getStatus() {
return {
isActive: this.isActive,
suppressEndTime: this.suppressEndTime,
suppressedCount: this.suppressedMessages.length,
settings: { ...this.settings }
};
}
}
/**
* Navbar Do Not Disturb Integration
* Verbindet den DND-Button in der Navbar mit dem DoNotDisturbManager
*/
class NavbarDNDIntegration {
constructor(dndManager) {
this.dndManager = dndManager;
this.button = document.getElementById('dndToggle');
this.counter = document.getElementById('dndCounter');
this.iconOff = null;
this.iconOn = null;
this.tooltipOff = null;
this.tooltipOn = null;
this.init();
}
/**
* Navbar DND Integration initialisieren
*/
init() {
if (!this.button) {
console.log(' DND Button nicht gefunden - Navbar Integration deaktiviert');
return;
}
this.iconOff = this.button.querySelector('.dnd-icon-off');
this.iconOn = this.button.querySelector('.dnd-icon-on');
this.tooltipOff = this.button.querySelector('.dnd-tooltip-off');
this.tooltipOn = this.button.querySelector('.dnd-tooltip-on');
// Event Listener
this.button.addEventListener('click', () => this.handleButtonClick());
// Initial state setzen
this.updateButton();
// Status-Änderungen überwachen
setInterval(() => this.updateButton(), 1000);
console.log('🔕 Navbar DND Integration erfolgreich initialisiert');
}
/**
* Button-Click Handler
*/
handleButtonClick() {
this.dndManager.toggle();
this.updateButton();
}
/**
* Button-Erscheinungsbild aktualisieren
*/
updateButton() {
if (!this.button) return;
const status = this.dndManager.getStatus();
if (status.isActive) {
// DND ist aktiv
this.button.classList.add('dnd-active');
if (this.iconOff) {
this.iconOff.style.opacity = '0';
this.iconOff.style.transform = 'scale(0.75)';
}
if (this.iconOn) {
this.iconOn.style.opacity = '1';
this.iconOn.style.transform = 'scale(1)';
}
if (this.tooltipOff) this.tooltipOff.classList.add('hidden');
if (this.tooltipOn) this.tooltipOn.classList.remove('hidden');
// Counter aktualisieren
if (this.counter && status.suppressedCount > 0) {
this.counter.textContent = status.suppressedCount > 99 ? '99+' : status.suppressedCount;
this.counter.classList.remove('hidden');
} else if (this.counter) {
this.counter.classList.add('hidden');
}
// Button-Erscheinungsbild
this.button.style.background = 'rgba(239, 68, 68, 0.1)';
this.button.style.borderColor = 'rgba(239, 68, 68, 0.3)';
} else {
// DND ist inaktiv
this.button.classList.remove('dnd-active');
if (this.iconOff) {
this.iconOff.style.opacity = '1';
this.iconOff.style.transform = 'scale(1)';
}
if (this.iconOn) {
this.iconOn.style.opacity = '0';
this.iconOn.style.transform = 'scale(0.75)';
}
if (this.tooltipOff) this.tooltipOff.classList.remove('hidden');
if (this.tooltipOn) this.tooltipOn.classList.add('hidden');
if (this.counter) this.counter.classList.add('hidden');
// Button-Erscheinungsbild zurücksetzen
this.button.style.background = '';
this.button.style.borderColor = '';
}
}
}
// Initialisierung aller UI-Komponenten
document.addEventListener('DOMContentLoaded', function() {
// Toast-Manager
window.MYP.UI.toast = new ToastManager();
// Modal-Manager
window.MYP.UI.modal = new ModalManager();
// Dropdown-Manager
window.MYP.UI.dropdown = new DropdownManager();
// Loading-Manager
window.MYP.UI.loading = new LoadingManager();
// 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();
// Connection Status Manager
window.MYP.UI.connectionStatus = new ConnectionStatusManager();
// Navbar Scroll Manager für Glassmorphism-Effekte
window.MYP.UI.navbarScroll = new NavbarScrollManager();
// Do Not Disturb Manager
window.MYP.UI.doNotDisturb = new DoNotDisturbManager();
// Navbar DND Integration
window.MYP.UI.navbarDND = new NavbarDNDIntegration(window.MYP.UI.doNotDisturb);
// Convenience-Methoden
window.showToast = (message, type, duration) => window.MYP.UI.toast.show(message, type, duration);
window.showModal = (modalId, options) => window.MYP.UI.modal.open(modalId, options);
window.hideModal = (modalId) => window.MYP.UI.modal.close(modalId);
window.toggleDarkMode = () => window.MYP.UI.darkMode.setDarkMode(!window.MYP.UI.darkMode.isDarkMode());
window.toggleDoNotDisturb = () => window.MYP.UI.doNotDisturb.toggle();
window.enableDoNotDisturb = (duration) => window.MYP.UI.doNotDisturb.enable(duration);
window.disableDoNotDisturb = () => window.MYP.UI.doNotDisturb.disable();
// Event-Listener für data-Attribute
document.addEventListener('click', (e) => {
// Modal-Trigger
if (e.target.matches('[data-modal-open]')) {
const modalId = e.target.getAttribute('data-modal-open');
window.MYP.UI.modal.open(modalId);
}
// Modal-Close
if (e.target.matches('[data-modal-close]')) {
const modalId = e.target.getAttribute('data-modal-close');
window.MYP.UI.modal.close(modalId);
}
// Dropdown-Toggle
if (e.target.closest('[data-dropdown-toggle]')) {
const dropdownId = e.target.closest('[data-dropdown-toggle]').getAttribute('data-dropdown-toggle');
window.MYP.UI.dropdown.toggle(dropdownId);
}
// Do Not Disturb Toggle
if (e.target.matches('[data-dnd-toggle]') || e.target.closest('[data-dnd-toggle]')) {
window.MYP.UI.doNotDisturb.toggle();
}
});
// Keyboard Shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + Shift + D für Do Not Disturb Toggle
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'd') {
e.preventDefault();
window.MYP.UI.doNotDisturb.toggle();
}
// Escape für alle Modals schließen
if (e.key === 'Escape') {
window.MYP.UI.modal.closeTopModal();
}
});
console.log('✅ MYP UI Components erfolgreich initialisiert - Erweiterte Benutzeroberfläche mit Glassmorphism und Do Not Disturb bereit');
// Test der glasigen Flash Messages (nur im Development)
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
// Warten auf vollständige Initialisierung, dann Test-Messages anzeigen
setTimeout(() => {
console.log('🧪 Teste glasige Flash Messages...');
showFlashMessage('Glassmorphism Flash Messages sind aktiv! ✨', 'success', 7000);
setTimeout(() => {
showFlashMessage('Do Not Disturb System ist bereit. Probieren Sie es im Footer aus! 🔕', 'info', 7000);
}, 1000);
setTimeout(() => {
showFlashMessage('Warnung: Dies ist eine Test-Nachricht für das glasige Design.', 'warning', 7000);
}, 2000);
}, 2000);
}
});
// Globale Variablen für erweiterte Flash Messages
window.showFlashMessage = showFlashMessage;
window.closeFlashMessage = closeFlashMessage;
// Globale Variable für Toast-Funktion
window.showToast = showToast;
// Globale Variable für API-Aufrufe
window.apiCall = apiCall;
})();