📝 "🐛 Refactor backend files, improve documentation, and update UI components (#123)"
This commit is contained in:
parent
070f4a6165
commit
4dd3c4b1b1
@ -5439,13 +5439,48 @@ def api_admin_database_status():
|
|||||||
table_stats[table] = f"Error: {str(e)}"
|
table_stats[table] = f"Error: {str(e)}"
|
||||||
|
|
||||||
# Connection-Pool-Status
|
# Connection-Pool-Status
|
||||||
pool_status = {
|
pool_status = {}
|
||||||
'pool_size': engine.pool.size(),
|
try:
|
||||||
'checked_in': engine.pool.checkedin(),
|
# StaticPool hat andere Methoden als andere Pool-Typen
|
||||||
'checked_out': engine.pool.checkedout(),
|
if hasattr(engine.pool, 'size'):
|
||||||
'overflow': engine.pool.overflow(),
|
pool_status['pool_size'] = engine.pool.size()
|
||||||
'invalid': engine.pool.invalid()
|
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': 'Error',
|
||||||
|
'checked_in': 'Error',
|
||||||
|
'checked_out': 'Error',
|
||||||
|
'overflow': 'Error',
|
||||||
|
'invalid': 'Error',
|
||||||
|
'pool_type': type(engine.pool).__name__,
|
||||||
|
'error': str(pool_error)
|
||||||
|
}
|
||||||
|
|
||||||
db_session.close()
|
db_session.close()
|
||||||
|
|
||||||
@ -5567,10 +5602,20 @@ def api_admin_system_status():
|
|||||||
try:
|
try:
|
||||||
boot_time = psutil.boot_time()
|
boot_time = psutil.boot_time()
|
||||||
uptime_seconds = int(time.time() - 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 = {
|
uptime_info = {
|
||||||
'boot_time': datetime.fromtimestamp(boot_time).isoformat(),
|
'boot_time': datetime.fromtimestamp(boot_time).isoformat(),
|
||||||
'uptime_seconds': uptime_seconds,
|
'uptime_seconds': uptime_seconds,
|
||||||
'uptime_formatted': str(timedelta(seconds=uptime_seconds))
|
'uptime_formatted': uptime_formatted
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
uptime_info = {'error': 'Uptime-Informationen nicht verfügbar'}
|
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 */
|
/* Glassmorphism Flash Messages */
|
||||||
.flash-message {
|
.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;
|
@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;
|
||||||
backdrop-filter: blur(20px) saturate(180%) brightness(120%);
|
/* Verstärkter Glassmorphism-Effekt */
|
||||||
-webkit-backdrop-filter: blur(20px) saturate(180%) brightness(120%);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
backdrop-filter: blur(40px) saturate(200%) brightness(130%) contrast(110%);
|
||||||
animation: slide-down 0.3s ease-out;
|
-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 {
|
.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 {
|
.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 {
|
.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 {
|
.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 {
|
@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} message - Nachrichtentext
|
||||||
* @param {string} type - Nachrichtentyp (success, error, info, warning)
|
* @param {string} type - Nachrichtentyp (success, error, info, warning)
|
||||||
|
* @param {number} duration - Anzeigedauer in Millisekunden (Standard: 5000)
|
||||||
*/
|
*/
|
||||||
function showToast(message, type = 'info') {
|
function showFlashMessage(message, type = 'info', duration = 5000) {
|
||||||
// Prüfen, ob Toast-Container existiert
|
// Unique ID für die Nachricht
|
||||||
let toastContainer = document.getElementById('toast-container');
|
const messageId = 'flash-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
// Falls nicht, erstellen wir einen
|
// Flash-Message-Element erstellen
|
||||||
if (!toastContainer) {
|
const flashElement = document.createElement('div');
|
||||||
toastContainer = document.createElement('div');
|
flashElement.id = messageId;
|
||||||
toastContainer.id = 'toast-container';
|
flashElement.className = `flash-message ${type}`;
|
||||||
toastContainer.className = 'fixed top-4 right-4 z-50 flex flex-col space-y-2';
|
|
||||||
document.body.appendChild(toastContainer);
|
// 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
|
// Inhalt der Flash Message
|
||||||
const toast = document.createElement('div');
|
flashElement.innerHTML = `
|
||||||
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)}`;
|
<div class="flex items-center">
|
||||||
toast.innerHTML = `
|
${icon}
|
||||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 ${getToastIconClass(type)}">
|
<div class="flex-1">
|
||||||
${getToastIcon(type)}
|
<p class="font-medium text-sm">${message}</p>
|
||||||
|
</div>
|
||||||
|
<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>
|
</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
|
// Flash Message zum DOM hinzufügen
|
||||||
toastContainer.appendChild(toast);
|
document.body.appendChild(flashElement);
|
||||||
|
|
||||||
// Schließen-Button-Event
|
// Flash Messages vertikal stapeln
|
||||||
const closeButton = toast.querySelector('button');
|
repositionFlashMessages();
|
||||||
closeButton.addEventListener('click', () => {
|
|
||||||
dismissToast(toast);
|
// 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
|
// Initialisierung aller UI-Komponenten
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Toast-Manager
|
// Toast-Manager
|
||||||
@ -1253,11 +1795,20 @@
|
|||||||
// Navbar Scroll Manager für Glassmorphism-Effekte
|
// Navbar Scroll Manager für Glassmorphism-Effekte
|
||||||
window.MYP.UI.navbarScroll = new NavbarScrollManager();
|
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
|
// Convenience-Methoden
|
||||||
window.showToast = (message, type, duration) => window.MYP.UI.toast.show(message, type, duration);
|
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.showModal = (modalId, options) => window.MYP.UI.modal.open(modalId, options);
|
||||||
window.hideModal = (modalId) => window.MYP.UI.modal.close(modalId);
|
window.hideModal = (modalId) => window.MYP.UI.modal.close(modalId);
|
||||||
window.toggleDarkMode = () => window.MYP.UI.darkMode.setDarkMode(!window.MYP.UI.darkMode.isDarkMode());
|
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
|
// Event-Listener für data-Attribute
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
@ -1278,17 +1829,37 @@
|
|||||||
const dropdownId = e.target.closest('[data-dropdown-toggle]').getAttribute('data-dropdown-toggle');
|
const dropdownId = e.target.closest('[data-dropdown-toggle]').getAttribute('data-dropdown-toggle');
|
||||||
window.MYP.UI.dropdown.toggle(dropdownId);
|
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
|
// Globale Variable für Toast-Funktion
|
||||||
window.showToast = showToast;
|
window.showToast = showToast;
|
||||||
|
|
||||||
// Globale Variable für API-Aufrufe
|
// Globale Variable für API-Aufrufe
|
||||||
window.apiCall = apiCall;
|
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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<!-- Benachrichtigungen - kompakteres Design -->
|
<!-- Benachrichtigungen - kompakteres Design -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user