Files
Projektarbeit-MYP/backend/templates/dashboard.html

1602 lines
66 KiB
HTML

{% extends "base.html" %}
{% block title %}Dashboard - Mercedes-Benz MYP Platform{% endblock %}
{% block extra_css %}
<style>
/* Professional Mercedes-Benz Dashboard Styles */
.mb-dashboard-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.dark .mb-dashboard-card {
background: var(--mb-black, #000000); /* Schwarzer Hintergrund im Dark Mode */
border-color: var(--border-primary, #334155);
}
.mb-dashboard-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.dark .mb-dashboard-card:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
}
.mb-stat-card {
background: linear-gradient(135deg, #f0f9ff 0%, #e6f2ff 100%);
color: #0f172a;
position: relative;
overflow: hidden;
border: none;
}
.dark .mb-stat-card {
background: linear-gradient(135deg, #000000 0%, #111827 100%); /* Mercedes Schwarz */
color: var(--text-primary, #f8fafc);
}
.mb-stat-card::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(0, 114, 206, 0.1) 0%, transparent 70%);
animation: pulse-glow 4s ease-in-out infinite;
}
.dark .mb-stat-card::before {
background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 0%, transparent 70%); /* Subtle glow for dark */
}
@keyframes pulse-glow {
0%, 100% { opacity: 0.5; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.05); }
}
.mb-stat-icon {
background: rgba(0, 0, 0, 0.1); /* Schwarz statt Blau */
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 10;
transition: all 0.3s ease;
}
.dark .mb-stat-icon {
background: rgba(255, 255, 255, 0.1); /* Lighter icon background for contrast */
}
.mb-stat-card:hover .mb-stat-icon {
transform: scale(1.1);
background: rgba(0, 0, 0, 0.2); /* Schwarz statt Blau */
}
.dark .mb-stat-card:hover .mb-stat-icon {
background: rgba(255, 255, 255, 0.15);
}
.mb-status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
position: relative;
display: inline-block;
}
.mb-status-indicator::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
animation: status-pulse 2s infinite;
}
.mb-status-online {
background: #10b981;
}
.mb-status-online::after {
background: #10b981;
}
.mb-status-busy {
background: #f59e0b;
}
.mb-status-busy::after {
background: #f59e0b;
}
.mb-status-offline {
background: #ef4444;
}
.mb-status-offline::after {
background: #ef4444;
}
.mb-status-idle {
background: #6b7280;
}
.mb-status-idle::after {
background: #6b7280;
}
@keyframes status-pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.5; }
100% { transform: scale(2); opacity: 0; }
}
.mb-progress-container {
background: #f3f4f6;
height: 8px;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.dark .mb-progress-container {
background: #374151;
}
.mb-progress-bar {
background: linear-gradient(90deg, #000000 0%, #333333 100%); /* Schwarz statt Blau */
height: 100%;
border-radius: 4px;
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.dark .mb-progress-bar {
background: linear-gradient(90deg, #f3f4f6 0%, #e5e7eb 100%);
}
.mb-progress-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: progress-shine 2s infinite;
}
@keyframes progress-shine {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.mb-activity-item {
border-left: 4px solid #000000; /* Schwarz statt Blau */
background: linear-gradient(90deg, rgba(0, 0, 0, 0.05) 0%, transparent 100%); /* Schwarz statt Blau */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 0 8px 8px 0;
}
.dark .mb-activity-item {
border-left-color: #f3f4f6;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%);
}
/* Light/Dark Mode compatible cards */
.glass-card-light {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(229, 231, 235, 0.8);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.dark .glass-card-light {
background: rgba(0, 0, 0, 0.8);
border-color: rgba(100, 116, 139, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
.section-title {
color: #1e293b;
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
.dark .section-title {
color: #ffffff;
}
.stat-value {
color: #0f172a;
font-weight: 700;
font-size: 1.5rem;
}
.dark .stat-value {
color: #ffffff;
}
.stat-label {
color: #64748b;
font-size: 0.875rem;
}
.dark .stat-label {
color: #94a3b8;
}
</style>
{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Dashboard Header Card -->
<div class="dashboard-card p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="flex items-center gap-6">
<div class="w-16 h-16 flex-shrink-0">
<svg class="w-full h-full text-slate-900 dark:text-white" fill="currentColor" viewBox="0 0 80 80" aria-hidden="true">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Dashboard</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">Übersicht über Ihre 3D-Druck Aktivitäten</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<button id="refreshDashboard"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Aktualisieren</span>
</button>
<a href="{{ url_for('jobs_page') }}"
class="btn-primary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<span>Neuer Auftrag</span>
</a>
<a href="{{ url_for('guest.guest_requests_overview') }}"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span>Anträge Übersicht</span>
</a>
</div>
</div>
</div>
<!-- Stats Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Active Jobs Card -->
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Aktive Aufträge</h3>
<div class="stat-value">{{ active_jobs_count }}</div>
</div>
<div class="mb-stat-icon text-slate-900 dark:text-white">
<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="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
</div>
</div>
</div>
<!-- Available Printers Card -->
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Verfügbare Drucker</h3>
<div class="stat-value">{{ available_printers_count }}</div>
</div>
<div class="mb-stat-icon text-green-600 dark:text-green-400">
<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="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
</div>
</div>
</div>
<!-- Total Jobs Card -->
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Aufträge (gesamt)</h3>
<div class="stat-value">{{ total_jobs_count }}</div>
</div>
<div class="mb-stat-icon text-purple-600 dark:text-purple-400">
<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="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
</div>
<!-- Success Rate Card -->
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Erfolgsrate</h3>
<div class="stat-value">{{ success_rate }}%</div>
</div>
<div class="mb-stat-icon text-amber-600 dark:text-amber-400">
<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="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
</div>
</div>
</div>
</div>
<!-- Timer Management Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Session Timer -->
<div class="dashboard-card p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="section-title mb-0">Session-Timer</h2>
<div class="flex items-center gap-2">
<div class="mb-status-indicator mb-status-idle" id="session-timer-status"></div>
<span class="text-sm text-slate-500 dark:text-slate-400">Gestoppt</span>
</div>
</div>
<!-- Timer Container -->
<div id="session-countdown-timer" class="mb-6"></div>
<!-- Timer Configuration -->
<div class="bg-gray-50 dark:bg-slate-700/30 rounded-lg p-4 mb-4">
<h4 class="text-sm font-medium text-slate-900 dark:text-white mb-3">Timer-Einstellungen</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs text-slate-500 dark:text-slate-400 mb-1">Dauer (Minuten)</label>
<select id="session-duration" class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-md bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
<option value="30">30 Minuten</option>
<option value="60">1 Stunde</option>
<option value="120" selected>2 Stunden</option>
<option value="240">4 Stunden</option>
<option value="480">8 Stunden</option>
</select>
</div>
<div>
<label class="block text-xs text-slate-500 dark:text-slate-400 mb-1">Force-Quit Aktion</label>
<select id="session-force-quit" class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-md bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
<option value="logout" selected>Automatisch abmelden</option>
<option value="warning">Nur warnen</option>
</select>
</div>
</div>
<div class="mt-3">
<label class="block text-xs text-slate-500 dark:text-slate-400 mb-1">Warnung (Sekunden vor Ablauf)</label>
<input type="number" id="session-warning" value="60" min="10" max="300" class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-md bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
</div>
</div>
<!-- Quick Actions -->
<div class="flex flex-wrap gap-2">
<button id="create-session-timer" class="btn-primary text-sm px-4 py-2">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Timer erstellen
</button>
<button id="extend-session-5min" class="btn-secondary text-sm px-4 py-2">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
+5 Min
</button>
<button id="extend-session-15min" class="btn-secondary text-sm px-4 py-2">
+15 Min
</button>
</div>
</div>
<!-- Kiosk Timer (Admin Only) -->
{% if current_user.is_admin %}
<div class="dashboard-card p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="section-title mb-0">Kiosk-Timer</h2>
<div class="flex items-center gap-2">
<div class="mb-status-indicator mb-status-idle" id="kiosk-timer-status"></div>
<span class="text-sm text-slate-500 dark:text-slate-400">Inaktiv</span>
</div>
</div>
<!-- Timer Container -->
<div id="kiosk-countdown-timer" class="mb-6"></div>
<!-- Admin Configuration -->
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4">
<div class="flex items-center mb-3">
<svg class="w-5 h-5 text-amber-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<h4 class="text-sm font-medium text-amber-800 dark:text-amber-200">Administrator-Funktion</h4>
</div>
<p class="text-xs text-amber-700 dark:text-amber-300 mb-3">Kiosk-Timer gelten systemweit für alle Benutzer und führen bei Ablauf automatische Aktionen aus.</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs text-amber-700 dark:text-amber-300 mb-1">Dauer (Minuten)</label>
<select id="kiosk-duration" class="w-full px-3 py-2 text-sm border border-amber-300 dark:border-amber-700 rounded-md bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
<option value="15">15 Minuten</option>
<option value="30" selected>30 Minuten</option>
<option value="60">1 Stunde</option>
<option value="120">2 Stunden</option>
</select>
</div>
<div>
<label class="block text-xs text-amber-700 dark:text-amber-300 mb-1">System-Aktion</label>
<select id="kiosk-action" class="w-full px-3 py-2 text-sm border border-amber-300 dark:border-amber-700 rounded-md bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
<option value="logout" selected>Alle Benutzer abmelden</option>
<option value="restart">System neustarten</option>
<option value="shutdown">System herunterfahren</option>
</select>
</div>
</div>
</div>
<!-- Admin Actions -->
<div class="flex flex-wrap gap-2">
<button id="create-kiosk-timer" class="btn-warning text-sm px-4 py-2">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Kiosk-Timer starten
</button>
<button id="stop-kiosk-timer" class="btn-danger text-sm px-4 py-2">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
</svg>
Stoppen
</button>
<button id="force-quit-now" class="btn-danger text-sm px-4 py-2">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
Force-Quit jetzt
</button>
</div>
</div>
{% else %}
<!-- Timer Overview for Non-Admin -->
<div class="dashboard-card p-6">
<h2 class="section-title">Timer-Übersicht</h2>
<div id="timer-overview" class="space-y-4">
<!-- Dynamic timer list will be populated here -->
<div class="text-center py-8">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine aktiven Timer</p>
<p class="text-slate-500 dark:text-slate-400 text-sm">Erstellen Sie einen Session-Timer für automatische Verwaltung.</p>
</div>
</div>
</div>
{% endif %}
</div>
<!-- Active Jobs Section -->
<div class="dashboard-card p-6">
<h2 class="section-title">Aktuelle Druckaufträge</h2>
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
<thead>
<tr class="text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
<th class="px-6 py-3">Status</th>
<th class="px-6 py-3">Auftrag</th>
<th class="px-6 py-3">Drucker</th>
<th class="px-6 py-3">Startzeit</th>
<th class="px-6 py-3">Fortschritt</th>
<th class="px-6 py-3">Aktionen</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
{% if active_jobs and active_jobs|length > 0 %}
{% for job in active_jobs %}
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="mb-status-indicator {{ job.status_class }}"></div>
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">
{{ job.status_text }}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ job.name }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">{{ job.file_name }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.printer }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.start_time }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="mb-progress-container">
<div class="mb-progress-bar" style="width: {{ job.progress }}%"></div>
</div>
<div class="text-xs text-right mt-1 text-slate-500 dark:text-slate-400">{{ job.progress }}%</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<a href="{{ url_for('job_detail', job_id=job.id) }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 font-medium">Details</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="px-6 py-8 text-center">
<div class="text-slate-500 dark:text-slate-400">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine aktiven Druckaufträge</p>
<p class="text-slate-500 dark:text-slate-400 text-sm">Starten Sie einen neuen Druckauftrag, um ihn hier zu sehen.</p>
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="mt-4 text-right">
<a href="{{ url_for('jobs_page') }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 text-sm font-medium">
Alle Druckaufträge anzeigen →
</a>
</div>
</div>
<!-- Last Row: Printer Status and Recent Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Available Printers -->
<div class="dashboard-card p-6">
<h2 class="section-title">Druckerstatus</h2>
<div class="space-y-4">
{% if printers and printers|length > 0 %}
{% for printer in printers %}
<div class="flex items-center justify-between p-4 rounded-xl bg-gray-50 dark:bg-slate-700/30">
<div class="flex items-center">
<div class="mb-status-indicator {{ printer.status_class }} mr-3"></div>
<div>
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ printer.name }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.model }}</div>
</div>
</div>
<div class="text-right">
<div class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ printer.status_text }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.location }}</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-8">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine Drucker gefunden</p>
<p class="text-slate-500 dark:text-slate-400 text-sm">Prüfen Sie die Verbindung oder fügen Sie Drucker hinzu.</p>
</div>
{% endif %}
</div>
<div class="mt-4 text-right">
<a href="{{ url_for('printers_page') }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 text-sm font-medium">
Alle Drucker anzeigen →
</a>
</div>
</div>
<!-- Recent Activity -->
<div class="dashboard-card p-6">
<h2 class="section-title">Letzte Aktivitäten</h2>
<div class="space-y-3">
{% if activities and activities|length > 0 %}
{% for activity in activities %}
<div class="mb-activity-item pl-4 py-3">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-slate-700 dark:text-slate-300">{{ activity.description }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">{{ activity.time }}</p>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-8">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine Aktivitäten</p>
<p class="text-slate-500 dark:text-slate-400 text-sm">Ihre Aktivitäten werden hier angezeigt.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/countdown-timer.js') }}"></script>
<script>
class DashboardManager {
constructor() {
this.updateInterval = 30000; // 30 Sekunden
this.autoUpdateTimer = null;
this.isUpdating = false;
this.wsConnection = null;
this.init();
}
init() {
this.setupEventListeners();
this.setupAutoUpdate();
this.setupWebSocket();
this.animateCounters();
console.log('🚀 Dashboard Manager initialisiert');
}
setupEventListeners() {
// Refresh Button
const refreshBtn = document.getElementById('refreshDashboard');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.refreshDashboard();
});
}
// Job-Zeilen klickbar machen für Details
this.setupJobRowClicks();
// Drucker-Karten klickbar machen
this.setupPrinterClicks();
// Dark Mode Updates
window.addEventListener('darkModeChanged', (e) => {
this.updateThemeElements(e.detail.isDark);
});
// Visibility Change Detection für intelligente Updates
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pauseAutoUpdate();
} else {
this.resumeAutoUpdate();
this.refreshDashboard(); // Einmalige Aktualisierung bei Rückkehr
}
});
}
setupJobRowClicks() {
const jobRows = document.querySelectorAll('tbody tr[data-job-id]');
jobRows.forEach(row => {
row.style.cursor = 'pointer';
row.addEventListener('click', () => {
const jobId = row.dataset.jobId;
if (jobId) {
window.location.href = `/jobs/${jobId}`;
}
});
// Hover-Effekt verstärken
row.addEventListener('mouseenter', () => {
row.style.transform = 'scale(1.01)';
row.style.transition = 'transform 0.2s ease';
});
row.addEventListener('mouseleave', () => {
row.style.transform = 'scale(1)';
});
});
}
setupPrinterClicks() {
const printerCards = document.querySelectorAll('[data-printer-id]');
printerCards.forEach(card => {
card.style.cursor = 'pointer';
card.addEventListener('click', () => {
const printerId = card.dataset.printerId;
if (printerId) {
window.location.href = `/printers/${printerId}`;
}
});
});
}
setupAutoUpdate() {
this.autoUpdateTimer = setInterval(() => {
if (!document.hidden && !this.isUpdating) {
this.updateDashboardData();
}
}, this.updateInterval);
}
pauseAutoUpdate() {
if (this.autoUpdateTimer) {
clearInterval(this.autoUpdateTimer);
this.autoUpdateTimer = null;
}
}
resumeAutoUpdate() {
if (!this.autoUpdateTimer) {
this.setupAutoUpdate();
}
}
setupWebSocket() {
// WebSocket für Real-time Updates
try {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/dashboard`;
this.wsConnection = new WebSocket(wsUrl);
this.wsConnection.onopen = () => {
console.log('📡 WebSocket Verbindung zu Dashboard hergestellt');
};
this.wsConnection.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleWebSocketUpdate(data);
} catch (error) {
console.error('Fehler beim Verarbeiten der WebSocket-Nachricht:', error);
}
};
this.wsConnection.onclose = () => {
console.log('📡 WebSocket Verbindung geschlossen - versuche Wiederverbindung in 5s');
setTimeout(() => {
this.setupWebSocket();
}, 5000);
};
this.wsConnection.onerror = (error) => {
console.error('WebSocket Fehler:', error);
};
} catch (error) {
console.log('WebSocket nicht verfügbar, verwende Polling-Updates');
}
}
handleWebSocketUpdate(data) {
console.log('📡 Erhaltenes WebSocket-Update:', data);
switch (data.type) {
case 'job_status_update':
this.updateJobStatus(data.job_id, data.status, data.progress);
break;
case 'printer_status_update':
this.updatePrinterStatus(data.printer_id, data.status);
break;
case 'new_activity':
this.addNewActivity(data.activity);
break;
case 'stats_update':
this.updateStats(data.stats);
break;
default:
console.log('Unbekannter WebSocket-Update-Typ:', data.type);
}
}
async refreshDashboard() {
if (this.isUpdating) return;
this.isUpdating = true;
const refreshBtn = document.getElementById('refreshDashboard');
try {
// Button-Status aktualisieren
if (refreshBtn) {
refreshBtn.disabled = true;
refreshBtn.innerHTML = `
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Aktualisiert...</span>
`;
}
// Vollständige Seitenaktualisierung
window.location.reload();
} catch (error) {
console.error('Fehler beim Aktualisieren:', error);
this.showToast('Fehler beim Aktualisieren des Dashboards', 'error');
} finally {
this.isUpdating = false;
// Button zurücksetzen
if (refreshBtn) {
refreshBtn.disabled = false;
refreshBtn.innerHTML = `
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Aktualisieren</span>
`;
}
}
}
async updateDashboardData() {
if (this.isUpdating) return;
this.isUpdating = true;
try {
// Stille Hintergrund-Updates ohne vollständige Neuladen
const responses = await Promise.all([
fetch('/api/dashboard/stats'),
fetch('/api/dashboard/active-jobs'),
fetch('/api/dashboard/printers'),
fetch('/api/dashboard/activities')
]);
const [statsData, jobsData, printersData, activitiesData] = await Promise.all(
responses.map(r => r.json())
);
// UI-Updates
this.updateStats(statsData);
this.updateActiveJobs(jobsData);
this.updatePrinters(printersData);
this.updateActivities(activitiesData);
console.log('🔄 Dashboard-Daten erfolgreich aktualisiert');
} catch (error) {
console.error('Fehler beim Laden der Dashboard-Daten:', error);
// Fehlschlag stumm - verwende WebSocket oder warte auf nächste Aktualisierung
} finally {
this.isUpdating = false;
}
}
updateJobStatus(jobId, status, progress) {
const jobRow = document.querySelector(`tr[data-job-id="${jobId}"]`);
if (jobRow) {
// Status-Indikator aktualisieren
const statusIndicator = jobRow.querySelector('.mb-status-indicator');
const statusText = jobRow.querySelector('.text-sm.font-medium');
const progressBar = jobRow.querySelector('.mb-progress-bar');
const progressText = jobRow.querySelector('.text-xs.text-right');
if (statusIndicator) {
statusIndicator.className = `mb-status-indicator ${this.getStatusClass(status)}`;
}
if (statusText) {
statusText.textContent = this.getStatusText(status);
}
if (progressBar && progress !== undefined) {
progressBar.style.width = `${progress}%`;
}
if (progressText && progress !== undefined) {
progressText.textContent = `${progress}%`;
}
// Animation für Updates
jobRow.style.backgroundColor = 'rgba(59, 130, 246, 0.1)';
setTimeout(() => {
jobRow.style.backgroundColor = '';
}, 1000);
}
}
updatePrinterStatus(printerId, status) {
const printerCard = document.querySelector(`[data-printer-id="${printerId}"]`);
if (printerCard) {
const statusIndicator = printerCard.querySelector('.mb-status-indicator');
const statusText = printerCard.querySelector('.text-sm.font-medium:last-child');
if (statusIndicator) {
statusIndicator.className = `mb-status-indicator ${this.getStatusClass(status)}`;
}
if (statusText) {
statusText.textContent = this.getStatusText(status);
}
// Animation für Updates
printerCard.style.transform = 'scale(1.02)';
setTimeout(() => {
printerCard.style.transform = 'scale(1)';
}, 300);
}
}
addNewActivity(activity) {
const activitiesContainer = document.querySelector('.space-y-3');
if (activitiesContainer) {
const newActivity = document.createElement('div');
newActivity.className = 'mb-activity-item pl-4 py-3 opacity-0';
newActivity.innerHTML = `
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-slate-700 dark:text-slate-300">${activity.description}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">${activity.time}</p>
</div>
</div>
`;
// Am Anfang einfügen
activitiesContainer.insertBefore(newActivity, activitiesContainer.firstChild);
// Animation
setTimeout(() => {
newActivity.style.opacity = '1';
newActivity.style.transition = 'opacity 0.5s ease';
}, 100);
// Alte Aktivitäten entfernen (max 10)
const activities = activitiesContainer.querySelectorAll('.mb-activity-item');
if (activities.length > 10) {
activities[activities.length - 1].remove();
}
}
}
updateStats(stats) {
// Statistik-Karten aktualisieren mit Animation
const statValues = document.querySelectorAll('.stat-value');
const statsMapping = [
{ element: statValues[0], value: stats.active_jobs_count },
{ element: statValues[1], value: stats.available_printers_count },
{ element: statValues[2], value: stats.total_jobs_count },
{ element: statValues[3], value: `${stats.success_rate}%` }
];
statsMapping.forEach(({ element, value }) => {
if (element && element.textContent !== value.toString()) {
this.animateValueChange(element, value);
}
});
}
animateValueChange(element, newValue) {
element.style.transform = 'scale(1.1)';
element.style.transition = 'transform 0.3s ease';
setTimeout(() => {
element.textContent = newValue;
element.style.transform = 'scale(1)';
}, 150);
}
animateCounters() {
// Initialanimation für Counter
const counters = document.querySelectorAll('.stat-value');
counters.forEach((counter, index) => {
const finalValue = parseInt(counter.textContent) || 0;
if (finalValue > 0) {
let currentValue = 0;
const increment = finalValue / 30; // 30 Schritte
const timer = setInterval(() => {
currentValue += increment;
if (currentValue >= finalValue) {
counter.textContent = finalValue + (counter.textContent.includes('%') ? '%' : '');
clearInterval(timer);
} else {
counter.textContent = Math.floor(currentValue) + (counter.textContent.includes('%') ? '%' : '');
}
}, 50 + (index * 100)); // Verzögerung zwischen Countern
}
});
}
getStatusClass(status) {
const statusClasses = {
'running': 'mb-status-busy',
'completed': 'mb-status-online',
'failed': 'mb-status-offline',
'paused': 'mb-status-idle',
'queued': 'mb-status-idle',
'online': 'mb-status-online',
'offline': 'mb-status-offline',
'busy': 'mb-status-busy',
'idle': 'mb-status-idle'
};
return statusClasses[status] || 'mb-status-idle';
}
getStatusText(status) {
const statusTexts = {
'running': 'Druckt',
'completed': 'Abgeschlossen',
'failed': 'Fehlgeschlagen',
'paused': 'Pausiert',
'queued': 'Warteschlange',
'online': 'Online',
'offline': 'Offline',
'busy': 'Beschäftigt',
'idle': 'Bereit'
};
return statusTexts[status] || 'Unbekannt';
}
updateThemeElements(isDark) {
// Theme-spezifische Updates
console.log(`🎨 Dashboard Theme aktualisiert: ${isDark ? 'Dark' : 'Light'} Mode`);
// Spezielle Animationen für Theme-Wechsel
const cards = document.querySelectorAll('.dashboard-card');
cards.forEach(card => {
card.style.transition = 'all 0.3s ease';
});
}
showToast(message, type = 'info') {
// Toast-Benachrichtigung anzeigen
if (window.MYP && window.MYP.UI && window.MYP.UI.ToastManager) {
const toast = new window.MYP.UI.ToastManager();
toast.show(message, type, 5000);
} else {
console.log(`Toast: ${message}`);
}
}
// Cleanup beim Verlassen der Seite
cleanup() {
if (this.autoUpdateTimer) {
clearInterval(this.autoUpdateTimer);
}
if (this.wsConnection) {
this.wsConnection.close();
}
}
}
// Dashboard Manager initialisieren
document.addEventListener('DOMContentLoaded', function() {
window.dashboardManager = new DashboardManager();
// Timer-Funktionalität initialisieren
initializeTimerControls();
// Cleanup beim Verlassen
window.addEventListener('beforeunload', () => {
if (window.dashboardManager) {
window.dashboardManager.cleanup();
}
// Timer cleanup
if (window.TimerManager) {
window.TimerManager.destroyAll();
}
});
});
/**
* Initialisiert die Timer-Steuerung im Dashboard
*/
function initializeTimerControls() {
console.log('🕒 Timer-Steuerung wird initialisiert...');
// Session-Timer Ereignisse
setupSessionTimerEvents();
// Kiosk-Timer Ereignisse (nur für Admins)
if (document.getElementById('create-kiosk-timer')) {
setupKioskTimerEvents();
}
// Bestehende Timer laden
loadExistingTimers();
// Timer-Status regelmäßig aktualisieren
setInterval(updateTimerStatus, 5000);
console.log('✅ Timer-Steuerung erfolgreich initialisiert');
}
/**
* Konfiguriert Session-Timer Ereignisse
*/
function setupSessionTimerEvents() {
// Timer erstellen
const createBtn = document.getElementById('create-session-timer');
if (createBtn) {
createBtn.addEventListener('click', async function() {
const duration = parseInt(document.getElementById('session-duration').value);
const forceQuitAction = document.getElementById('session-force-quit').value;
const warningThreshold = parseInt(document.getElementById('session-warning').value);
await createSessionTimer(duration, forceQuitAction, warningThreshold);
});
}
// Timer verlängern - 5 Minuten
const extend5Btn = document.getElementById('extend-session-5min');
if (extend5Btn) {
extend5Btn.addEventListener('click', async function() {
await extendSessionTimer(300); // 5 Minuten = 300 Sekunden
});
}
// Timer verlängern - 15 Minuten
const extend15Btn = document.getElementById('extend-session-15min');
if (extend15Btn) {
extend15Btn.addEventListener('click', async function() {
await extendSessionTimer(900); // 15 Minuten = 900 Sekunden
});
}
}
/**
* Konfiguriert Kiosk-Timer Ereignisse (Admin)
*/
function setupKioskTimerEvents() {
// Kiosk-Timer erstellen
const createKioskBtn = document.getElementById('create-kiosk-timer');
if (createKioskBtn) {
createKioskBtn.addEventListener('click', async function() {
const duration = parseInt(document.getElementById('kiosk-duration').value);
const action = document.getElementById('kiosk-action').value;
await createKioskTimer(duration, action);
});
}
// Kiosk-Timer stoppen
const stopKioskBtn = document.getElementById('stop-kiosk-timer');
if (stopKioskBtn) {
stopKioskBtn.addEventListener('click', async function() {
await stopKioskTimer();
});
}
// Force-Quit sofort ausführen
const forceQuitBtn = document.getElementById('force-quit-now');
if (forceQuitBtn) {
forceQuitBtn.addEventListener('click', async function() {
if (confirm('⚠️ Force-Quit wird sofort ausgeführt. Alle Benutzer werden abgemeldet. Fortfahren?')) {
await executeForceQuitNow();
}
});
}
}
/**
* Erstellt einen Session-Timer
*/
async function createSessionTimer(durationMinutes, forceQuitAction, warningThreshold) {
try {
showTimerLoading('session');
// API-Aufruf zur Timer-Erstellung
const response = await fetch('/api/timers/session/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify({
duration_minutes: durationMinutes
})
});
const result = await response.json();
if (result.success) {
// Timer-UI erstellen
const timer = window.TimerManager.create('session_timer', {
container: 'session-countdown-timer',
duration: durationMinutes * 60,
forceQuitAction: forceQuitAction,
warningThreshold: warningThreshold,
syncWithServer: true,
apiBase: '/api/timers/session_timer_' + getCurrentUserId(),
size: 'medium',
theme: 'primary',
warningMessage: 'Ihre Session läuft ab! Speichern Sie Ihre Arbeit.',
autoStart: true,
// Callbacks
onTick: (remaining, total) => {
updateTimerStatusIndicator('session', 'running');
},
onWarning: (remaining) => {
showTimerWarning('Session läuft in ' + Math.floor(remaining / 60) + ' Minuten ab!');
updateTimerStatusIndicator('session', 'warning');
},
onExpired: () => {
updateTimerStatusIndicator('session', 'expired');
if (forceQuitAction === 'logout') {
showLogoutWarning();
}
},
onForceQuit: () => {
handleSessionForceQuit(forceQuitAction);
}
});
showTimerSuccess('Session-Timer erfolgreich erstellt und gestartet');
updateSessionTimerControls(true);
} else {
throw new Error(result.error || 'Timer konnte nicht erstellt werden');
}
} catch (error) {
console.error('Fehler beim Erstellen des Session-Timers:', error);
showTimerError('Session-Timer konnte nicht erstellt werden: ' + error.message);
} finally {
hideTimerLoading('session');
}
}
/**
* Erstellt einen Kiosk-Timer (Admin)
*/
async function createKioskTimer(durationMinutes, action) {
try {
showTimerLoading('kiosk');
const response = await fetch('/api/timers/kiosk/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify({
duration_minutes: durationMinutes,
auto_start: true
})
});
const result = await response.json();
if (result.success) {
// Kiosk-Timer UI erstellen
const timer = window.TimerManager.create('kiosk_timer', {
container: 'kiosk-countdown-timer',
duration: durationMinutes * 60,
forceQuitAction: action,
warningThreshold: 30,
syncWithServer: true,
apiBase: '/api/timers/kiosk_session',
size: 'medium',
theme: 'warning',
warningMessage: '⚠️ Kiosk-Session läuft ab! System wird automatisch ' + getActionText(action) + '.',
autoStart: true,
showControls: true,
onTick: (remaining, total) => {
updateTimerStatusIndicator('kiosk', 'running');
broadcastKioskTimerUpdate(remaining, total);
},
onWarning: (remaining) => {
showKioskWarning('Kiosk-Timer läuft in ' + Math.floor(remaining / 60) + ' Minuten ab!');
updateTimerStatusIndicator('kiosk', 'warning');
},
onExpired: () => {
updateTimerStatusIndicator('kiosk', 'expired');
executeKioskAction(action);
}
});
showTimerSuccess('Kiosk-Timer erfolgreich gestartet');
updateKioskTimerControls(true);
} else {
throw new Error(result.error || 'Kiosk-Timer konnte nicht erstellt werden');
}
} catch (error) {
console.error('Fehler beim Erstellen des Kiosk-Timers:', error);
showTimerError('Kiosk-Timer konnte nicht erstellt werden: ' + error.message);
} finally {
hideTimerLoading('kiosk');
}
}
/**
* Verlängert Session-Timer
*/
async function extendSessionTimer(additionalSeconds) {
try {
const timer = window.TimerManager.get('session_timer');
if (!timer) {
showTimerError('Kein aktiver Session-Timer gefunden');
return;
}
const success = await timer.extend(additionalSeconds);
if (success) {
const minutes = Math.floor(additionalSeconds / 60);
showTimerSuccess(`Session-Timer um ${minutes} Minuten verlängert`);
}
} catch (error) {
console.error('Fehler beim Verlängern des Session-Timers:', error);
showTimerError('Timer konnte nicht verlängert werden');
}
}
/**
* Stoppt Kiosk-Timer
*/
async function stopKioskTimer() {
try {
const timer = window.TimerManager.get('kiosk_timer');
if (!timer) {
showTimerError('Kein aktiver Kiosk-Timer gefunden');
return;
}
const success = await timer.stop();
if (success) {
showTimerSuccess('Kiosk-Timer gestoppt');
updateKioskTimerControls(false);
updateTimerStatusIndicator('kiosk', 'stopped');
}
} catch (error) {
console.error('Fehler beim Stoppen des Kiosk-Timers:', error);
showTimerError('Kiosk-Timer konnte nicht gestoppt werden');
}
}
/**
* Führt Force-Quit sofort aus
*/
async function executeForceQuitNow() {
try {
const response = await fetch('/api/timers/kiosk_session/force-quit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
}
});
const result = await response.json();
if (result.success) {
showTimerSuccess('Force-Quit wird ausgeführt...');
// Je nach Aktion unterschiedlich behandeln
if (result.action === 'logout') {
setTimeout(() => {
window.location.href = result.redirect_url || '/login';
}, 2000);
} else {
showTimerSuccess('Force-Quit-Aktion ausgeführt: ' + result.action);
}
} else {
throw new Error(result.error || 'Force-Quit fehlgeschlagen');
}
} catch (error) {
console.error('Fehler beim Force-Quit:', error);
showTimerError('Force-Quit konnte nicht ausgeführt werden');
}
}
/**
* Lädt bestehende Timer beim Seitenaufruf
*/
async function loadExistingTimers() {
try {
const response = await fetch('/api/timers');
const result = await response.json();
if (result.success && result.data) {
result.data.forEach(timerData => {
if (timerData.status === 'running') {
recreateTimerFromData(timerData);
}
});
}
} catch (error) {
console.error('Fehler beim Laden bestehender Timer:', error);
}
}
/**
* Recreiert Timer aus Server-Daten
*/
function recreateTimerFromData(timerData) {
const isSessionTimer = timerData.timer_type === 'session';
const isKioskTimer = timerData.timer_type === 'kiosk';
if (isSessionTimer && timerData.context_id === getCurrentUserId()) {
// Session-Timer für aktuellen Benutzer recreieren
const timer = window.TimerManager.create('session_timer', {
container: 'session-countdown-timer',
duration: timerData.duration_seconds,
remaining: timerData.remaining_seconds,
forceQuitAction: timerData.force_quit_action,
warningThreshold: timerData.force_quit_warning_seconds,
syncWithServer: true,
autoStart: false, // Läuft bereits
size: 'medium',
theme: 'primary'
});
updateTimerStatusIndicator('session', 'running');
updateSessionTimerControls(true);
} else if (isKioskTimer && isCurrentUserAdmin()) {
// Kiosk-Timer für Admin recreieren
const timer = window.TimerManager.create('kiosk_timer', {
container: 'kiosk-countdown-timer',
duration: timerData.duration_seconds,
remaining: timerData.remaining_seconds,
forceQuitAction: timerData.force_quit_action,
warningThreshold: timerData.force_quit_warning_seconds,
syncWithServer: true,
autoStart: false, // Läuft bereits
size: 'medium',
theme: 'warning'
});
updateTimerStatusIndicator('kiosk', 'running');
updateKioskTimerControls(true);
}
}
/**
* Aktualisiert Timer-Status regelmäßig
*/
async function updateTimerStatus() {
// Implementierung für regelmäßige Status-Updates
const sessionTimer = window.TimerManager.get('session_timer');
const kioskTimer = window.TimerManager.get('kiosk_timer');
if (sessionTimer && sessionTimer.config.syncWithServer) {
sessionTimer.syncWithServer();
}
if (kioskTimer && kioskTimer.config.syncWithServer) {
kioskTimer.syncWithServer();
}
}
// Hilfsfunktionen
function updateTimerStatusIndicator(timerType, status) {
const indicator = document.getElementById(`${timerType}-timer-status`);
const statusText = indicator?.nextElementSibling;
if (indicator) {
indicator.className = `mb-status-indicator ${getTimerStatusClass(status)}`;
}
if (statusText) {
statusText.textContent = getTimerStatusText(status);
}
}
function getTimerStatusClass(status) {
const classes = {
'running': 'mb-status-busy',
'stopped': 'mb-status-idle',
'warning': 'mb-status-busy',
'expired': 'mb-status-offline'
};
return classes[status] || 'mb-status-idle';
}
function getTimerStatusText(status) {
const texts = {
'running': 'Läuft',
'stopped': 'Gestoppt',
'warning': 'Warnung',
'expired': 'Abgelaufen'
};
return texts[status] || 'Unbekannt';
}
function updateSessionTimerControls(active) {
const createBtn = document.getElementById('create-session-timer');
const extendBtns = [
document.getElementById('extend-session-5min'),
document.getElementById('extend-session-15min')
];
if (createBtn) createBtn.disabled = active;
extendBtns.forEach(btn => {
if (btn) btn.disabled = !active;
});
}
function updateKioskTimerControls(active) {
const createBtn = document.getElementById('create-kiosk-timer');
const stopBtn = document.getElementById('stop-kiosk-timer');
const forceQuitBtn = document.getElementById('force-quit-now');
if (createBtn) createBtn.disabled = active;
if (stopBtn) stopBtn.disabled = !active;
if (forceQuitBtn) forceQuitBtn.disabled = !active;
}
function showTimerLoading(timerType) {
// Implementierung für Loading-Anzeige
}
function hideTimerLoading(timerType) {
// Implementierung für Loading-Anzeige ausblenden
}
function showTimerSuccess(message) {
if (window.dashboardManager) {
window.dashboardManager.showToast(message, 'success');
}
}
function showTimerError(message) {
if (window.dashboardManager) {
window.dashboardManager.showToast(message, 'error');
}
}
function showTimerWarning(message) {
if (window.dashboardManager) {
window.dashboardManager.showToast(message, 'warning');
}
}
function getActionText(action) {
const actions = {
'logout': 'abgemeldet',
'restart': 'neugestartet',
'shutdown': 'heruntergefahren'
};
return actions[action] || action;
}
function getCurrentUserId() {
return {{ current_user.id }};
}
function isCurrentUserAdmin() {
return {{ 'true' if current_user.is_admin else 'false' }};
}
function getCSRFToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
</script>
{% endblock %}