- Removed `COMMON_ERRORS.md` file to streamline documentation. - Added `Flask-Limiter` for rate limiting and `redis` for session management in `requirements.txt`. - Expanded `ROADMAP.md` to include completed security features and planned enhancements for version 2.2. - Enhanced `setup_myp.sh` for ultra-secure kiosk installation, including system hardening and security configurations. - Updated `app.py` to integrate CSRF protection and improved logging setup. - Refactored user model to include username and active status for better user management. - Improved job scheduler with uptime tracking and task management features. - Updated various templates for a more cohesive user interface and experience.
1039 lines
42 KiB
JavaScript
1039 lines
42 KiB
JavaScript
/**
|
|
* 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.darkModeIcon = this.darkModeToggle ? this.darkModeToggle.querySelector('svg') : 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');
|
|
} 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');
|
|
}
|
|
|
|
// 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('Dark Mode set to:', enable);
|
|
}
|
|
|
|
/**
|
|
* Icon für Dark Mode Toggle aktualisieren
|
|
* @param {boolean} isDark - Ob Dark Mode aktiv ist
|
|
*/
|
|
updateDarkModeIcon(isDark) {
|
|
if (!this.darkModeIcon) return;
|
|
|
|
if (isDark) {
|
|
// Sonne anzeigen (für Wechsel zum Light Mode)
|
|
this.darkModeIcon.innerHTML = `
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
`;
|
|
} else {
|
|
// Mond anzeigen (für Wechsel zum Dark Mode)
|
|
this.darkModeIcon.innerHTML = `
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event Listener einrichten und Darkmode initialisieren
|
|
*/
|
|
init() {
|
|
if (!this.darkModeToggle) {
|
|
console.error('Dark Mode Toggle Button nicht gefunden!');
|
|
return;
|
|
}
|
|
|
|
console.log('Dark Mode Manager initialisiert');
|
|
|
|
// Event Listener für den Dark Mode Toggle Button
|
|
this.darkModeToggle.addEventListener('click', () => {
|
|
console.log('Dark Mode Toggle geklickt');
|
|
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 geklickt');
|
|
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('Initial Dark Mode Status:', isDark);
|
|
this.setDarkMode(isDark);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toast-Nachricht anzeigen
|
|
* @param {string} message - Nachrichtentext
|
|
* @param {string} type - Nachrichtentyp (success, error, info, warning)
|
|
*/
|
|
function showToast(message, type = 'info') {
|
|
// Prüfen, ob Toast-Container existiert
|
|
let toastContainer = document.getElementById('toast-container');
|
|
|
|
// Falls nicht, erstellen wir einen
|
|
if (!toastContainer) {
|
|
toastContainer = document.createElement('div');
|
|
toastContainer.id = 'toast-container';
|
|
toastContainer.className = 'fixed top-4 right-4 z-50 flex flex-col space-y-2';
|
|
document.body.appendChild(toastContainer);
|
|
}
|
|
|
|
// Toast-Element erstellen
|
|
const toast = document.createElement('div');
|
|
toast.className = `flex items-center p-4 mb-4 text-sm rounded-lg shadow-lg transition-all transform translate-x-0 opacity-100 ${getToastTypeClass(type)}`;
|
|
toast.innerHTML = `
|
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 ${getToastIconClass(type)}">
|
|
${getToastIcon(type)}
|
|
</div>
|
|
<div class="ml-3 text-sm font-normal">${message}</div>
|
|
<button type="button" class="ml-auto -mx-1.5 -my-1.5 rounded-lg p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-2 focus:ring-gray-300 dark:focus:ring-gray-600 inline-flex h-8 w-8" aria-label="Schließen">
|
|
<span class="sr-only">Schließen</span>
|
|
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
<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>
|
|
`;
|
|
|
|
// Toast zum Container hinzufügen
|
|
toastContainer.appendChild(toast);
|
|
|
|
// Schließen-Button-Event
|
|
const closeButton = toast.querySelector('button');
|
|
closeButton.addEventListener('click', () => {
|
|
dismissToast(toast);
|
|
});
|
|
|
|
// Toast nach 5 Sekunden automatisch ausblenden
|
|
setTimeout(() => {
|
|
dismissToast(toast);
|
|
}, 5000);
|
|
}
|
|
|
|
/**
|
|
* Toast ausblenden und nach Animation entfernen
|
|
* @param {HTMLElement} toast - Toast-Element
|
|
*/
|
|
function dismissToast(toast) {
|
|
toast.classList.replace('translate-x-0', 'translate-x-full');
|
|
toast.classList.replace('opacity-100', 'opacity-0');
|
|
|
|
setTimeout(() => {
|
|
toast.remove();
|
|
}, 300);
|
|
}
|
|
|
|
/**
|
|
* CSS-Klassen für Toast-Typ
|
|
* @param {string} type - Nachrichtentyp
|
|
* @returns {string} CSS-Klassen
|
|
*/
|
|
function getToastTypeClass(type) {
|
|
switch (type) {
|
|
case 'success':
|
|
return 'text-green-800 bg-green-50 dark:bg-green-900/30 dark:text-green-300';
|
|
case 'error':
|
|
return 'text-red-800 bg-red-50 dark:bg-red-900/30 dark:text-red-300';
|
|
case 'warning':
|
|
return 'text-yellow-800 bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300';
|
|
case 'info':
|
|
default:
|
|
return 'text-blue-800 bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CSS-Klassen für Toast-Icon
|
|
* @param {string} type - Nachrichtentyp
|
|
* @returns {string} CSS-Klassen
|
|
*/
|
|
function getToastIconClass(type) {
|
|
switch (type) {
|
|
case 'success':
|
|
return 'bg-green-100 text-green-500 dark:bg-green-800 dark:text-green-200 rounded-lg';
|
|
case 'error':
|
|
return 'bg-red-100 text-red-500 dark:bg-red-800 dark:text-red-200 rounded-lg';
|
|
case 'warning':
|
|
return 'bg-yellow-100 text-yellow-500 dark:bg-yellow-800 dark:text-yellow-200 rounded-lg';
|
|
case 'info':
|
|
default:
|
|
return 'bg-blue-100 text-blue-500 dark:bg-blue-800 dark:text-blue-200 rounded-lg';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SVG-Icon für Toast-Typ
|
|
* @param {string} type - Nachrichtentyp
|
|
* @returns {string} SVG-Markup
|
|
*/
|
|
function getToastIcon(type) {
|
|
switch (type) {
|
|
case 'success':
|
|
return '<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>';
|
|
case 'error':
|
|
return '<svg aria-hidden="true" class="w-5 h-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>';
|
|
case 'warning':
|
|
return '<svg aria-hidden="true" class="w-5 h-5" 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>';
|
|
case 'info':
|
|
default:
|
|
return '<svg aria-hidden="true" class="w-5 h-5" 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>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flash-Nachricht anzeigen
|
|
* @param {string} message - Nachrichtentext
|
|
* @param {string} type - Nachrichtentyp (success, error, info, warning)
|
|
*/
|
|
function showFlashMessage(message, type = 'info') {
|
|
// Toast-Funktion verwenden, wenn verfügbar
|
|
if (typeof showToast === 'function') {
|
|
showToast(message, type);
|
|
} else {
|
|
// Fallback-Lösung, wenn Toast nicht verfügbar ist
|
|
alert(message);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
|
|
// User Dropdown Manager
|
|
window.MYP.UI.userDropdown = new UserDropdownManager();
|
|
|
|
// Connection Status Manager
|
|
window.MYP.UI.connectionStatus = new ConnectionStatusManager();
|
|
|
|
// 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());
|
|
|
|
// 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);
|
|
}
|
|
});
|
|
|
|
console.log('MYP UI Components initialized successfully');
|
|
});
|
|
|
|
// Globale Variable für Toast-Funktion
|
|
window.showToast = showToast;
|
|
|
|
// Globale Variable für API-Aufrufe
|
|
window.apiCall = apiCall;
|
|
|
|
// Globale Variable für Flash-Nachrichten
|
|
window.showFlashMessage = showFlashMessage;
|
|
})();
|