Till Tomczak 2d33753b94 feat: Major updates to backend structure and security enhancements
- 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.
2025-05-25 20:33:38 +02:00

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;
})();