📝 "🐛 Refactor backend files, improve documentation, and update UI components (#123)"
This commit is contained in:
parent
070f4a6165
commit
4dd3c4b1b1
@ -5439,12 +5439,47 @@ def api_admin_database_status():
|
||||
table_stats[table] = f"Error: {str(e)}"
|
||||
|
||||
# Connection-Pool-Status
|
||||
pool_status = {}
|
||||
try:
|
||||
# StaticPool hat andere Methoden als andere Pool-Typen
|
||||
if hasattr(engine.pool, 'size'):
|
||||
pool_status['pool_size'] = engine.pool.size()
|
||||
else:
|
||||
pool_status['pool_size'] = 'N/A (StaticPool)'
|
||||
|
||||
if hasattr(engine.pool, 'checkedin'):
|
||||
pool_status['checked_in'] = engine.pool.checkedin()
|
||||
else:
|
||||
pool_status['checked_in'] = 'N/A'
|
||||
|
||||
if hasattr(engine.pool, 'checkedout'):
|
||||
pool_status['checked_out'] = engine.pool.checkedout()
|
||||
else:
|
||||
pool_status['checked_out'] = 'N/A'
|
||||
|
||||
if hasattr(engine.pool, 'overflow'):
|
||||
pool_status['overflow'] = engine.pool.overflow()
|
||||
else:
|
||||
pool_status['overflow'] = 'N/A'
|
||||
|
||||
if hasattr(engine.pool, 'invalid'):
|
||||
pool_status['invalid'] = engine.pool.invalid()
|
||||
else:
|
||||
pool_status['invalid'] = 'N/A'
|
||||
|
||||
# Zusätzliche StaticPool-spezifische Informationen
|
||||
pool_status['pool_type'] = type(engine.pool).__name__
|
||||
|
||||
except Exception as pool_error:
|
||||
app_logger.warning(f"Fehler beim Abrufen des Pool-Status: {str(pool_error)}")
|
||||
pool_status = {
|
||||
'pool_size': engine.pool.size(),
|
||||
'checked_in': engine.pool.checkedin(),
|
||||
'checked_out': engine.pool.checkedout(),
|
||||
'overflow': engine.pool.overflow(),
|
||||
'invalid': engine.pool.invalid()
|
||||
'pool_size': 'Error',
|
||||
'checked_in': 'Error',
|
||||
'checked_out': 'Error',
|
||||
'overflow': 'Error',
|
||||
'invalid': 'Error',
|
||||
'pool_type': type(engine.pool).__name__,
|
||||
'error': str(pool_error)
|
||||
}
|
||||
|
||||
db_session.close()
|
||||
@ -5567,10 +5602,20 @@ def api_admin_system_status():
|
||||
try:
|
||||
boot_time = psutil.boot_time()
|
||||
uptime_seconds = int(time.time() - boot_time)
|
||||
|
||||
# Robuste uptime-Formatierung
|
||||
try:
|
||||
days = uptime_seconds // 86400
|
||||
hours = (uptime_seconds % 86400) // 3600
|
||||
minutes = ((uptime_seconds % 86400) % 3600) // 60
|
||||
uptime_formatted = f"{days}d {hours}h {minutes}m"
|
||||
except (ValueError, OverflowError, ZeroDivisionError):
|
||||
uptime_formatted = f"{uptime_seconds}s"
|
||||
|
||||
uptime_info = {
|
||||
'boot_time': datetime.fromtimestamp(boot_time).isoformat(),
|
||||
'uptime_seconds': uptime_seconds,
|
||||
'uptime_formatted': str(timedelta(seconds=uptime_seconds))
|
||||
'uptime_formatted': uptime_formatted
|
||||
}
|
||||
except Exception:
|
||||
uptime_info = {'error': 'Uptime-Informationen nicht verfügbar'}
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
backend/docs/FEHLER_BEHOBEN_ADMIN_API.md
Normal file
1
backend/docs/FEHLER_BEHOBEN_ADMIN_API.md
Normal file
@ -0,0 +1 @@
|
||||
|
1
backend/docs/GLASSMORPHISM_UND_DND_SYSTEM.md
Normal file
1
backend/docs/GLASSMORPHISM_UND_DND_SYSTEM.md
Normal file
@ -0,0 +1 @@
|
||||
|
1
backend/docs/PROBLEMBEHEBUNG_CALENDAR_ENDPOINTS.md
Normal file
1
backend/docs/PROBLEMBEHEBUNG_CALENDAR_ENDPOINTS.md
Normal file
@ -0,0 +1 @@
|
||||
|
@ -360,27 +360,237 @@
|
||||
|
||||
/* Glassmorphism Flash Messages */
|
||||
.flash-message {
|
||||
@apply fixed top-4 right-4 px-6 py-4 rounded-xl text-sm font-medium shadow-2xl transform transition-all duration-300 z-50 backdrop-blur-xl border border-white/20;
|
||||
backdrop-filter: blur(20px) saturate(180%) brightness(120%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%) brightness(120%);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
animation: slide-down 0.3s ease-out;
|
||||
@apply fixed top-4 right-4 px-6 py-4 rounded-2xl text-sm font-medium shadow-2xl transform transition-all duration-500 z-50 border;
|
||||
/* Verstärkter Glassmorphism-Effekt */
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(40px) saturate(200%) brightness(130%) contrast(110%);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(200%) brightness(130%) contrast(110%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
box-shadow:
|
||||
0 32px 64px rgba(0, 0, 0, 0.25),
|
||||
0 12px 24px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
animation: flash-slide-in 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dark .flash-message {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(40px) saturate(180%) brightness(120%) contrast(115%);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(180%) brightness(120%) contrast(115%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow:
|
||||
0 32px 64px rgba(0, 0, 0, 0.6),
|
||||
0 12px 24px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.flash-message:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow:
|
||||
0 40px 80px rgba(0, 0, 0, 0.3),
|
||||
0 16px 32px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.dark .flash-message:hover {
|
||||
box-shadow:
|
||||
0 40px 80px rgba(0, 0, 0, 0.7),
|
||||
0 16px 32px rgba(0, 0, 0, 0.5),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.flash-message.info {
|
||||
@apply bg-blue-500/70 dark:bg-blue-600/70 text-white;
|
||||
@apply text-blue-100;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(59, 130, 246, 0.2) 0%,
|
||||
rgba(147, 197, 253, 0.15) 50%,
|
||||
rgba(59, 130, 246, 0.1) 100%);
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.flash-message.success {
|
||||
@apply bg-green-500/70 dark:bg-green-600/70 text-white;
|
||||
@apply text-green-100;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(34, 197, 94, 0.2) 0%,
|
||||
rgba(134, 239, 172, 0.15) 50%,
|
||||
rgba(34, 197, 94, 0.1) 100%);
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.flash-message.warning {
|
||||
@apply bg-yellow-500/70 dark:bg-yellow-600/70 text-white;
|
||||
@apply text-yellow-100;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(245, 158, 11, 0.2) 0%,
|
||||
rgba(252, 211, 77, 0.15) 50%,
|
||||
rgba(245, 158, 11, 0.1) 100%);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.flash-message.error {
|
||||
@apply bg-red-500/70 dark:bg-red-600/70 text-white;
|
||||
@apply text-red-100;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(239, 68, 68, 0.2) 0%,
|
||||
rgba(252, 165, 165, 0.15) 50%,
|
||||
rgba(239, 68, 68, 0.1) 100%);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Flash Message Animation */
|
||||
@keyframes flash-slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(100%) translateY(-20px) scale(0.9);
|
||||
backdrop-filter: blur(0px);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: translateX(20px) translateY(-10px) scale(1.05);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0) translateY(0) scale(1);
|
||||
backdrop-filter: blur(40px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flash-slide-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateX(0) translateY(0) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(100%) translateY(-20px) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.flash-message.hiding {
|
||||
animation: flash-slide-out 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
/* Do Not Disturb System Styles */
|
||||
.dnd-toggle {
|
||||
@apply relative inline-flex items-center h-6 rounded-full w-11 transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
|
||||
background: rgba(156, 163, 175, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(156, 163, 175, 0.2);
|
||||
}
|
||||
|
||||
.dnd-toggle.active {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.dnd-toggle-slider {
|
||||
@apply inline-block h-4 w-4 rounded-full shadow-lg transform transition-transform duration-300;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow:
|
||||
0 4px 8px rgba(0, 0, 0, 0.2),
|
||||
0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin: 0.125rem;
|
||||
}
|
||||
|
||||
.dnd-toggle.active .dnd-toggle-slider {
|
||||
transform: translateX(1.25rem);
|
||||
background: rgba(255, 255, 255, 1);
|
||||
box-shadow:
|
||||
0 6px 12px rgba(239, 68, 68, 0.3),
|
||||
0 3px 6px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.dnd-indicator {
|
||||
@apply fixed top-4 left-4 z-50 flex items-center px-3 py-2 rounded-lg text-sm font-medium transition-all duration-300;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
backdrop-filter: blur(20px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(150%);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: rgb(239, 68, 68);
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dnd-indicator.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dnd-modal {
|
||||
@apply fixed inset-0 z-50 flex items-center justify-center p-4;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.dnd-modal-content {
|
||||
@apply w-full max-w-md rounded-2xl p-6 shadow-2xl transform transition-all;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(40px) saturate(200%) brightness(120%);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(200%) brightness(120%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow:
|
||||
0 25px 50px rgba(0, 0, 0, 0.25),
|
||||
0 8px 16px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.dark .dnd-modal-content {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(40px) saturate(180%) brightness(110%);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(180%) brightness(110%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow:
|
||||
0 25px 50px rgba(0, 0, 0, 0.6),
|
||||
0 8px 16px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* DND Flash Message Override */
|
||||
.flash-message.dnd-suppressed {
|
||||
animation: flash-fade-in 0.3s ease-out;
|
||||
opacity: 0.3;
|
||||
transform: scale(0.95);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes flash-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
/* Notification Counter */
|
||||
.dnd-counter {
|
||||
@apply absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-bold;
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
animation: dnd-counter-bounce 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dnd-counter-bounce {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-down {
|
||||
|
File diff suppressed because one or more lines are too long
2
backend/static/css/tailwind.min.css
vendored
2
backend/static/css/tailwind.min.css
vendored
File diff suppressed because one or more lines are too long
@ -951,136 +951,103 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast-Nachricht anzeigen
|
||||
* 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 showToast(message, type = 'info') {
|
||||
// Prüfen, ob Toast-Container existiert
|
||||
let toastContainer = document.getElementById('toast-container');
|
||||
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);
|
||||
|
||||
// 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);
|
||||
// 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" 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" 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" 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" 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>`;
|
||||
}
|
||||
|
||||
// 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)}
|
||||
// Inhalt der Flash Message
|
||||
flashElement.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
${icon}
|
||||
<div class="flex-1">
|
||||
<p class="font-medium text-sm">${message}</p>
|
||||
</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>
|
||||
<button class="flash-close-btn ml-4 text-current opacity-70 hover:opacity-100 transition-opacity duration-200"
|
||||
onclick="closeFlashMessage('${messageId}')">
|
||||
<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>
|
||||
`;
|
||||
|
||||
// Toast zum Container hinzufügen
|
||||
toastContainer.appendChild(toast);
|
||||
// Flash Message zum DOM hinzufügen
|
||||
document.body.appendChild(flashElement);
|
||||
|
||||
// Schließen-Button-Event
|
||||
const closeButton = toast.querySelector('button');
|
||||
closeButton.addEventListener('click', () => {
|
||||
dismissToast(toast);
|
||||
// Flash Messages vertikal stapeln
|
||||
repositionFlashMessages();
|
||||
|
||||
// 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)');
|
||||
flashMessages.forEach((flash, index) => {
|
||||
flash.style.top = `${16 + (index * 80)}px`; // 16px base + 80px pro Message
|
||||
flash.style.right = '16px';
|
||||
flash.style.zIndex = 50 - index; // Neueste Messages haben höheren z-index
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1224,6 +1191,581 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
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">
|
||||
<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">
|
||||
30 Min
|
||||
</button>
|
||||
<button class="dnd-quick-btn btn-primary" data-duration="60">
|
||||
1 Stunde
|
||||
</button>
|
||||
<button class="dnd-quick-btn btn-primary" data-duration="480">
|
||||
8 Stunden
|
||||
</button>
|
||||
<button class="dnd-quick-btn btn-primary" data-duration="0">
|
||||
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">
|
||||
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">
|
||||
Deaktivieren
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="dnd-close-btn btn-primary flex-1">
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event Listeners
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal || e.target.classList.contains('dnd-close-btn')) {
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('dnd-quick-btn')) {
|
||||
const duration = parseInt(e.target.dataset.duration);
|
||||
if (duration === 0) {
|
||||
this.enable();
|
||||
} else {
|
||||
this.enable(duration);
|
||||
}
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('dnd-disable-btn')) {
|
||||
this.disable();
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('dnd-clear-btn')) {
|
||||
this.clearSuppressedMessages();
|
||||
modal.remove();
|
||||
this.showSettings(); // Modal neu laden
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('dnd-setting')) {
|
||||
const setting = e.target.dataset.setting;
|
||||
this.settings[setting] = e.target.checked;
|
||||
this.saveSettings();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -1253,11 +1795,20 @@
|
||||
// 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) => {
|
||||
@ -1278,17 +1829,37 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ MYP UI Components erfolgreich initialisiert - Benutzeroberfläche bereit');
|
||||
// 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');
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
// Globale Variable für Flash-Nachrichten
|
||||
window.showFlashMessage = showFlashMessage;
|
||||
})();
|
347
backend/templates/admin_edit_printer.html
Normal file
347
backend/templates/admin_edit_printer.html
Normal file
@ -0,0 +1,347 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Drucker bearbeiten - MYP Admin</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div class="min-h-screen py-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i class="fas fa-edit text-blue-600 text-2xl"></i>
|
||||
<h1 class="text-2xl font-bold text-gray-800">Drucker bearbeiten</h1>
|
||||
</div>
|
||||
<a href="{{ url_for('admin_page', tab='printers') }}"
|
||||
class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-arrow-left mr-2"></i>Zurück
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-4 p-3 bg-blue-50 rounded-lg">
|
||||
<p class="text-sm text-blue-700">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<strong>Drucker-ID:</strong> {{ printer.id }} |
|
||||
<strong>Erstellt am:</strong> {{ printer.created_at[:10] if printer.created_at else 'Unbekannt' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formular -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<form action="{{ url_for('admin_update_printer_form', printer_id=printer.id) }}" method="POST" class="space-y-6">
|
||||
<!-- CSRF Token -->
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-tag mr-2"></i>Drucker-Name *
|
||||
</label>
|
||||
<input type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
value="{{ printer.name }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="3D-Drucker Raum A001">
|
||||
</div>
|
||||
|
||||
<!-- IP-Adresse -->
|
||||
<div>
|
||||
<label for="ip_address" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-network-wired mr-2"></i>IP-Adresse *
|
||||
</label>
|
||||
<input type="text"
|
||||
id="ip_address"
|
||||
name="ip_address"
|
||||
required
|
||||
value="{{ printer.plug_ip }}"
|
||||
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="192.168.1.100">
|
||||
<p class="text-sm text-gray-500 mt-1">IP-Adresse der Tapo-Steckdose</p>
|
||||
</div>
|
||||
|
||||
<!-- Modell -->
|
||||
<div>
|
||||
<label for="model" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-cogs mr-2"></i>Drucker-Modell
|
||||
</label>
|
||||
<input type="text"
|
||||
id="model"
|
||||
name="model"
|
||||
value="{{ printer.model }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Ender 3 V2">
|
||||
</div>
|
||||
|
||||
<!-- Standort -->
|
||||
<div>
|
||||
<label for="location" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-map-marker-alt mr-2"></i>Standort
|
||||
</label>
|
||||
<input type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
value="{{ printer.location }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Raum A001, Erdgeschoss">
|
||||
</div>
|
||||
|
||||
<!-- Beschreibung -->
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-comment mr-2"></i>Beschreibung
|
||||
</label>
|
||||
<textarea id="description"
|
||||
name="description"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Zusätzliche Informationen zum Drucker...">{{ printer.description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div>
|
||||
<label for="status" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-circle mr-2"></i>Aktueller Status
|
||||
</label>
|
||||
<select id="status"
|
||||
name="status"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
<option value="available" {{ 'selected' if printer.status == 'available' else '' }}>Verfügbar</option>
|
||||
<option value="offline" {{ 'selected' if printer.status == 'offline' else '' }}>Offline</option>
|
||||
<option value="maintenance" {{ 'selected' if printer.status == 'maintenance' else '' }}>Wartung</option>
|
||||
<option value="online" {{ 'selected' if printer.status == 'online' else '' }}>Online</option>
|
||||
<option value="printing" {{ 'selected' if printer.status == 'printing' else '' }}>Druckt</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Aktiv-Status -->
|
||||
<div>
|
||||
<label class="flex items-center space-x-3">
|
||||
<input type="checkbox"
|
||||
name="is_active"
|
||||
{{ 'checked' if printer.active else '' }}
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2">
|
||||
<span class="text-sm font-medium text-gray-700">
|
||||
<i class="fas fa-power-off mr-2"></i>Drucker aktiv
|
||||
</span>
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 mt-1">Inaktive Drucker werden nicht für neue Aufträge verwendet</p>
|
||||
</div>
|
||||
|
||||
<!-- Erweiterte Informationen -->
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3">
|
||||
<i class="fas fa-info-circle mr-2"></i>Drucker-Informationen
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">MAC-Adresse:</span>
|
||||
<span class="text-gray-800">{{ printer.mac_address or 'Nicht verfügbar' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Letzter Check:</span>
|
||||
<span class="text-gray-800">{{ printer.last_checked or 'Nie' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warnung -->
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<div class="flex">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-500 mt-0.5 mr-3"></i>
|
||||
<div class="text-sm text-yellow-700">
|
||||
<p class="font-semibold mb-1">Wichtige Hinweise:</p>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Änderungen an der IP-Adresse können die Verbindung unterbrechen</li>
|
||||
<li>Stellen Sie sicher, dass die Tapo-Steckdose unter der neuen IP erreichbar ist</li>
|
||||
<li>Bei Status-Änderungen werden laufende Jobs möglicherweise beeinflusst</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktionen -->
|
||||
<div class="flex space-x-3 pt-4">
|
||||
<button type="submit"
|
||||
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-save mr-2"></i>Änderungen speichern
|
||||
</button>
|
||||
<a href="{{ url_for('admin_page', tab='printers') }}"
|
||||
class="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg text-center transition-colors">
|
||||
<i class="fas fa-times mr-2"></i>Abbrechen
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Zusätzliche Aktionen -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-tools mr-2"></i>Drucker-Aktionen
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button data-printer-id="{{ printer.id }}"
|
||||
data-action="test"
|
||||
class="printer-action-btn bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-lg transition-colors">
|
||||
<i class="fas fa-plug mr-2"></i>Verbindung testen
|
||||
</button>
|
||||
<button data-printer-id="{{ printer.id }}"
|
||||
data-action="toggle"
|
||||
class="printer-action-btn bg-orange-600 hover:bg-orange-700 text-white px-4 py-3 rounded-lg transition-colors">
|
||||
<i class="fas fa-power-off mr-2"></i>Ein/Ausschalten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="fixed top-4 right-4 z-50 space-y-2">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else category }} bg-{{ 'red' if category == 'error' else 'green' }}-100 border border-{{ 'red' if category == 'error' else 'green' }}-400 text-{{ 'red' if category == 'error' else 'green' }}-700 px-4 py-3 rounded-lg shadow-md">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' }} mr-2"></i>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.querySelector('form');
|
||||
const nameInput = document.getElementById('name');
|
||||
const ipInput = document.getElementById('ip_address');
|
||||
|
||||
// IP-Adresse-Validierung
|
||||
ipInput.addEventListener('blur', function() {
|
||||
const ip = this.value;
|
||||
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
|
||||
if (ip && !ipRegex.test(ip)) {
|
||||
this.classList.add('border-red-500');
|
||||
this.classList.remove('border-gray-300');
|
||||
} else {
|
||||
this.classList.remove('border-red-500');
|
||||
this.classList.add('border-gray-300');
|
||||
}
|
||||
});
|
||||
|
||||
// Form-Submit-Validierung
|
||||
form.addEventListener('submit', function(e) {
|
||||
const name = nameInput.value.trim();
|
||||
const ip = ipInput.value.trim();
|
||||
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
|
||||
if (!name) {
|
||||
e.preventDefault();
|
||||
alert('Bitte geben Sie einen Drucker-Namen ein.');
|
||||
nameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ip || !ipRegex.test(ip)) {
|
||||
e.preventDefault();
|
||||
alert('Bitte geben Sie eine gültige IP-Adresse ein.');
|
||||
ipInput.focus();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Event-Listener für Drucker-Aktions-Buttons
|
||||
document.querySelectorAll('.printer-action-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const printerId = this.getAttribute('data-printer-id');
|
||||
const action = this.getAttribute('data-action');
|
||||
|
||||
if (action === 'test') {
|
||||
testPrinterConnection(printerId, this);
|
||||
} else if (action === 'toggle') {
|
||||
togglePrinterPower(printerId, this);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Verbindungstest
|
||||
function testPrinterConnection(printerId, button) {
|
||||
const originalText = button.innerHTML;
|
||||
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Teste...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/api/admin/printers/${printerId}/test-tapo`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.tapo_test && data.tapo_test.success) {
|
||||
alert('✅ Verbindung erfolgreich!\n\nStatus: ' + (data.tapo_test.device_info ? data.tapo_test.device_info.device_on ? 'EIN' : 'AUS' : 'Unbekannt'));
|
||||
} else {
|
||||
alert('❌ Verbindung fehlgeschlagen!\n\nFehler: ' + (data.tapo_test ? data.tapo_test.error : 'Unbekannter Fehler'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('❌ Verbindungstest fehlgeschlagen!\n\nFehler: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Drucker ein/ausschalten
|
||||
function togglePrinterPower(printerId, button) {
|
||||
const originalText = button.innerHTML;
|
||||
|
||||
if (!confirm('Möchten Sie den Drucker ein-/ausschalten?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Schaltet...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/api/admin/printers/${printerId}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('✅ Drucker erfolgreich ' + data.action + '!');
|
||||
// Seite neu laden um aktuellen Status zu zeigen
|
||||
location.reload();
|
||||
} else {
|
||||
alert('❌ Fehler beim Schalten!\n\nFehler: ' + (data.error || 'Unbekannter Fehler'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('❌ Schaltvorgang fehlgeschlagen!\n\nFehler: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -332,6 +332,7 @@
|
||||
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900 dark:border-t-slate-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<!-- Benachrichtigungen - kompakteres Design -->
|
||||
<div class="relative">
|
||||
|
Loading…
x
Reference in New Issue
Block a user