feat: Major updates to backend structure and security enhancements

- Removed `COMMON_ERRORS.md` file to streamline documentation.
- Added `Flask-Limiter` for rate limiting and `redis` for session management in `requirements.txt`.
- Expanded `ROADMAP.md` to include completed security features and planned enhancements for version 2.2.
- Enhanced `setup_myp.sh` for ultra-secure kiosk installation, including system hardening and security configurations.
- Updated `app.py` to integrate CSRF protection and improved logging setup.
- Refactored user model to include username and active status for better user management.
- Improved job scheduler with uptime tracking and task management features.
- Updated various templates for a more cohesive user interface and experience.
This commit is contained in:
2025-05-25 20:33:38 +02:00
parent e21104611f
commit 2d33753b94
1288 changed files with 247388 additions and 3249 deletions

View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}404 - Seite nicht gefunden - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
<!-- 404 Error Container -->
<div class="w-full max-w-md">
<div class="bg-white dark:bg-gray-800 backdrop-blur-xl bg-opacity-95 dark:bg-opacity-95 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 p-8 text-center transition-all duration-300">
<!-- Mercedes-Benz Logo -->
<div class="flex justify-center mb-6">
<div class="w-16 h-16 text-gray-300 dark:text-gray-600 transition-transform duration-500 hover:scale-110">
<svg class="w-full h-full" fill="currentColor" viewBox="0 0 80 80">
<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>
<!-- Error Message -->
<h1 class="text-6xl font-bold text-gray-300 dark:text-gray-600 mb-4">404</h1>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Seite nicht gefunden</h2>
<p class="text-gray-600 dark:text-gray-400 mb-6">Die von Ihnen gesuchte Seite existiert nicht oder wurde verschoben.</p>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-8">
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center justify-center px-5 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-all duration-300 transform hover:-translate-y-0.5">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>Zum Dashboard</span>
</a>
<button onclick="window.history.back()" class="inline-flex items-center justify-center px-5 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-all duration-300 transform hover:-translate-y-0.5">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
<span>Zurück</span>
</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,523 @@
{% extends "base.html" %}
{% block title %}Admin Panel - MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<!-- CSRF Token für AJAX-Anfragen -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin.css') }}">
<script src="{{ url_for('static', filename='js/admin.js') }}" defer></script>
{% endblock %}
{% block content %}
<div class="admin-container">
<h1 class="text-3xl font-bold mb-6 text-slate-900 dark:text-white">Admin Panel</h1>
<!-- Admin Stats -->
<div class="admin-stats">
<div class="stat-card">
<div class="stat-icon">
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
<div class="stat-title">Benutzer</div>
<div class="stat-value">{{ stats.total_users }}</div>
<div class="stat-desc">Registrierte Benutzer</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
</div>
<div class="stat-title">Drucker</div>
<div class="stat-value">{{ stats.total_printers }}</div>
<div class="stat-desc">Verbundene Drucker</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<div class="stat-title">Aktive Jobs</div>
<div class="stat-value">{{ stats.active_jobs }}</div>
<div class="stat-desc">Laufende Druckaufträge</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="stat-title">Erfolgsrate</div>
<div class="stat-value">{{ stats.success_rate }}%</div>
<div class="stat-desc">Erfolgreiche Druckaufträge</div>
</div>
</div>
<!-- Tabs Navigation -->
<div class="nav-tabs">
<a href="{{ url_for('admin_page', tab='users') }}" class="nav-tab {{ 'active' if active_tab == 'users' else '' }}">
<svg class="inline-block w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
Benutzer
</a>
<a href="{{ url_for('admin_page', tab='printers') }}" class="nav-tab {{ 'active' if active_tab == 'printers' else '' }}">
<svg class="inline-block w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
Drucker
</a>
<a href="{{ url_for('admin_page', tab='scheduler') }}" class="nav-tab {{ 'active' if active_tab == 'scheduler' else '' }}">
<svg class="inline-block w-5 h-5 mr-1" 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>
Scheduler
</a>
<a href="{{ url_for('admin_page', tab='system') }}" class="nav-tab {{ 'active' if active_tab == 'system' else '' }}">
<svg class="inline-block w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
System
</a>
<a href="{{ url_for('admin_page', tab='logs') }}" class="nav-tab {{ 'active' if active_tab == 'logs' else '' }}">
<svg class="inline-block w-5 h-5 mr-1" 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 2" />
</svg>
Logs
</a>
</div>
<!-- Tab Content -->
<div class="tab-content mt-8">
<!-- Users Tab -->
{% if active_tab == 'users' %}
<div id="users-tab" class="tab-pane active">
<div class="bg-white dark:bg-dark-card rounded-xl shadow-lg p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-slate-900 dark:text-white">Benutzer</h3>
<form action="{{ url_for('admin_page', tab='users') }}" method="get">
<button type="submit" class="btn btn-primary">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
Aktualisieren
</button>
</form>
</div>
<div class="overflow-x-auto rounded-lg border border-light-border dark:border-dark-border">
<table class="min-w-full divide-y divide-light-border dark:divide-dark-border">
<thead class="bg-slate-50 dark:bg-slate-800">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Benutzer</th>
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Rolle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Aktionen</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-dark-card divide-y divide-light-border dark:divide-dark-border">
{% for user in users %}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-700 dark:text-blue-300 font-medium">
{{ user.email[0]|upper if user.email else 'U' }}
</div>
<div class="ml-4">
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ user.name }}</div>
<div class="text-sm text-slate-500 dark:text-slate-400">{{ user.email }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' if user.is_admin else 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' }}">
{{ 'Administrator' if user.is_admin else 'Benutzer' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' if user.active else 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }}">
{{ 'Aktiv' if user.active else 'Inaktiv' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<form method="post" action="{{ url_for('delete_user', user_id=user.id) }}" class="inline" onsubmit="return confirm('Benutzer wirklich löschen?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Add User Form -->
<div class="mt-8 bg-slate-50 dark:bg-slate-800 rounded-lg p-6">
<h4 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Neuen Benutzer hinzufügen</h4>
<form action="{{ url_for('create_user') }}" method="post" class="space-y-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
<label for="user-email" class="form-label">Email</label>
<input type="email" id="user-email" name="email" class="form-input" required>
</div>
<div class="form-group">
<label for="user-name" class="form-label">Name</label>
<input type="text" id="user-name" name="name" class="form-input" required>
</div>
<div class="form-group">
<label for="user-password" class="form-label">Passwort</label>
<input type="password" id="user-password" name="password" class="form-input" required>
</div>
<div class="form-group">
<label for="user-role" class="form-label">Rolle</label>
<select id="user-role" name="role" class="form-select">
<option value="user">Benutzer</option>
<option value="admin">Administrator</option>
</select>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="btn btn-primary">Benutzer hinzufügen</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!-- Printers Tab -->
{% if active_tab == 'printers' %}
<div id="printers-tab" class="tab-pane active">
<div class="bg-white dark:bg-dark-card rounded-xl shadow-lg p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-slate-900 dark:text-white">Drucker</h3>
<form action="{{ url_for('admin_page', tab='printers') }}" method="get">
<button type="submit" class="btn btn-primary">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
Aktualisieren
</button>
</form>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for printer in printers %}
<div class="printer-card">
<div class="printer-header">
<h4 class="printer-name">{{ printer.name }}</h4>
<div class="printer-actions">
<form method="post" action="{{ url_for('delete_printer', printer_id=printer.id) }}" onsubmit="return confirm('Drucker wirklich löschen?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</form>
</div>
</div>
<div class="printer-info">
<div>
<span class="text-sm text-slate-500 dark:text-slate-400">Modell:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ printer.model }}</span>
</div>
<div>
<span class="text-sm text-slate-500 dark:text-slate-400">IP-Adresse:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ printer.ip_address }}</span>
</div>
<div>
<span class="text-sm text-slate-500 dark:text-slate-400">Standort:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ printer.location or 'Nicht angegeben' }}</span>
</div>
</div>
<div class="printer-status">
<div class="status-indicator {{ 'status-running' if printer.status == 'online' else 'status-stopped' }}"></div>
<span class="status-text">{{ 'Online' if printer.status == 'online' else 'Offline' }}</span>
</div>
</div>
{% endfor %}
</div>
<!-- Add Printer Form -->
<div class="mt-8 bg-slate-50 dark:bg-slate-800 rounded-lg p-6">
<h4 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Neuen Drucker hinzufügen</h4>
<form action="{{ url_for('create_printer') }}" method="post" class="space-y-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
<label for="printer-name" class="form-label">Name</label>
<input type="text" id="printer-name" name="name" class="form-input" required>
</div>
<div class="form-group">
<label for="printer-model" class="form-label">Modell</label>
<input type="text" id="printer-model" name="model" class="form-input" required>
</div>
<div class="form-group">
<label for="printer-mac" class="form-label">MAC-Adresse</label>
<input type="text" id="printer-mac" name="mac_address" class="form-input" required>
</div>
<div class="form-group">
<label for="printer-location" class="form-label">Standort</label>
<input type="text" id="printer-location" name="location" class="form-input">
</div>
<div class="form-group">
<label for="printer-plug-ip" class="form-label">Steckdosen-IP</label>
<input type="text" id="printer-plug-ip" name="plug_ip" class="form-input">
</div>
<div class="form-group">
<label for="printer-plug-username" class="form-label">Steckdosen-Benutzername</label>
<input type="text" id="printer-plug-username" name="plug_username" class="form-input">
</div>
<div class="form-group">
<label for="printer-plug-password" class="form-label">Steckdosen-Passwort</label>
<input type="password" id="printer-plug-password" name="plug_password" class="form-input">
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="btn btn-primary">Drucker hinzufügen</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!-- Scheduler Tab -->
{% if active_tab == 'scheduler' %}
<div id="scheduler-tab" class="tab-pane active">
<div class="bg-white dark:bg-dark-card rounded-xl shadow-lg p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-slate-900 dark:text-white">Scheduler Status</h3>
<div class="flex space-x-4">
<form action="{{ url_for('start_scheduler') }}" method="post" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-success">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Scheduler starten
</button>
</form>
<form action="{{ url_for('stop_scheduler') }}" method="post" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-error">
<svg class="w-5 h-5 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>
Scheduler stoppen
</button>
</form>
</div>
</div>
<div class="scheduler-status">
<div class="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 flex items-center justify-between">
<div class="flex items-center">
<div class="status-indicator {{ 'status-running pulse' if scheduler_status.running else 'status-stopped' }}"></div>
<span class="text-xl font-medium ml-3 {{ 'text-success' if scheduler_status.running else 'text-error' }}">
{{ 'Aktiv' if scheduler_status.running else 'Inaktiv' }}
</span>
</div>
<div class="text-slate-700 dark:text-slate-300">
{{ scheduler_status.message }}
</div>
</div>
<div class="mt-6 bg-slate-50 dark:bg-slate-800 rounded-lg p-6">
<h4 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Scheduler Informationen</h4>
<div class="space-y-4">
<p class="text-slate-700 dark:text-slate-300">
Der Scheduler ist verantwortlich für die automatische Zuweisung und Ausführung von Druckaufträgen.
Er überwacht kontinuierlich den Status der Drucker und der anstehenden Jobs.
</p>
<p class="text-slate-700 dark:text-slate-300">
<strong>Wichtige Hinweise:</strong>
<ul class="list-disc list-inside mt-2 space-y-1">
<li>Der Scheduler sollte während des normalen Betriebs immer aktiv sein</li>
<li>Bei Wartungsarbeiten kann der Scheduler vorübergehend deaktiviert werden</li>
<li>Nach einem Neustart des Systems muss der Scheduler manuell gestartet werden</li>
</ul>
</p>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- System Tab -->
{% if active_tab == 'system' %}
<div id="system-tab" class="tab-pane active">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white dark:bg-dark-card rounded-xl shadow-lg p-6">
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-6">System Ressourcen</h3>
<div class="space-y-6">
<div>
<div class="flex justify-between mb-2">
<span class="text-slate-700 dark:text-slate-300">CPU Auslastung</span>
<span class="text-slate-900 dark:text-white font-medium">{{ system_info.cpu }}%</span>
</div>
{{ render_progress_bar(system_info.cpu, 'blue')|safe }}
</div>
<div>
<div class="flex justify-between mb-2">
<span class="text-slate-700 dark:text-slate-300">Arbeitsspeicher</span>
<span class="text-slate-900 dark:text-white font-medium">{{ system_info.memory }}%</span>
</div>
{{ render_progress_bar(system_info.memory, 'green')|safe }}
</div>
<div>
<div class="flex justify-between mb-2">
<span class="text-slate-700 dark:text-slate-300">Festplattenspeicher</span>
<span class="text-slate-900 dark:text-white font-medium">{{ system_info.disk }}%</span>
</div>
{{ render_progress_bar(system_info.disk, 'purple')|safe }}
</div>
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<span class="text-slate-700 dark:text-slate-300">System Uptime</span>
<span class="text-slate-900 dark:text-white font-medium">{{ system_info.uptime }} Tage</span>
</div>
</div>
</div>
</div>
<div class="bg-white dark:bg-dark-card rounded-xl shadow-lg p-6">
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-6">Dienste Status</h3>
<div class="space-y-4">
<div class="flex justify-between items-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
<div class="flex items-center">
<div class="status-indicator status-running"></div>
<span class="ml-3 text-slate-900 dark:text-white">Webserver</span>
</div>
<span class="text-green-600 dark:text-green-400 font-medium">Online</span>
</div>
<div class="flex justify-between items-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
<div class="flex items-center">
<div class="status-indicator {{ 'status-running' if scheduler_status.running else 'status-stopped' }}"></div>
<span class="ml-3 text-slate-900 dark:text-white">Scheduler</span>
</div>
<span class="{{ 'text-green-600 dark:text-green-400' if scheduler_status.running else 'text-red-600 dark:text-red-400' }} font-medium">
{{ 'Online' if scheduler_status.running else 'Offline' }}
</span>
</div>
<div class="flex justify-between items-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
<div class="flex items-center">
<div class="status-indicator status-running"></div>
<span class="ml-3 text-slate-900 dark:text-white">Datenbank</span>
</div>
<span class="text-green-600 dark:text-green-400 font-medium">Online</span>
</div>
<div class="flex justify-between items-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
<div class="flex items-center">
<div class="status-indicator status-running"></div>
<span class="ml-3 text-slate-900 dark:text-white">Drucker-Manager</span>
</div>
<span class="text-green-600 dark:text-green-400 font-medium">Online</span>
</div>
</div>
<div class="mt-6">
<form action="{{ url_for('admin_page', tab='system') }}" method="get">
<button type="submit" class="btn btn-primary w-full">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
System-Informationen aktualisieren
</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Logs Tab -->
{% if active_tab == 'logs' %}
<div id="logs-tab" class="tab-pane active">
<div class="bg-white dark:bg-dark-card rounded-xl shadow-lg p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-slate-900 dark:text-white">System Logs</h3>
<div class="flex space-x-4">
<form action="{{ url_for('admin_page', tab='logs') }}" method="get">
<div class="flex space-x-2">
<select name="log_level" class="form-select rounded-lg border-light-border dark:border-dark-border text-slate-900 dark:text-white bg-white dark:bg-slate-800">
<option value="all" {{ 'selected' if request.args.get('log_level') == 'all' or not request.args.get('log_level') else '' }}>Alle Level</option>
<option value="ERROR" {{ 'selected' if request.args.get('log_level') == 'ERROR' else '' }}>Error</option>
<option value="WARNING" {{ 'selected' if request.args.get('log_level') == 'WARNING' else '' }}>Warning</option>
<option value="INFO" {{ 'selected' if request.args.get('log_level') == 'INFO' else '' }}>Info</option>
<option value="DEBUG" {{ 'selected' if request.args.get('log_level') == 'DEBUG' else '' }}>Debug</option>
</select>
<button type="submit" class="btn btn-secondary">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
Aktualisieren
</button>
</div>
</form>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-800 rounded-lg p-4 max-h-[600px] overflow-y-auto">
{% if logs %}
{% for log in logs %}
<div class="log-entry log-{{ log.level|lower }}">
<div class="flex items-start">
<div class="min-w-[160px] text-xs text-slate-500 dark:text-slate-400">{{ log.timestamp }}</div>
<div class="flex-1">
<div class="flex items-center">
<span class="badge badge-{{ 'success' if log.level == 'INFO' else ('warning' if log.level == 'WARNING' else ('error' if log.level == 'ERROR' else ('info' if log.level == 'DEBUG' else 'error'))) }}">
{{ log.level }}
</span>
<span class="ml-2 text-xs font-medium text-slate-700 dark:text-slate-300">[{{ log.category }}]</span>
</div>
<div class="mt-1 text-sm text-slate-700 dark:text-slate-300">{{ log.message }}</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-slate-700 dark:text-slate-300 p-4">Keine Logs gefunden.</p>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- Minimale JavaScript-Funktionalität -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Bestätigungen für Löschaktionen
const confirmForms = document.querySelectorAll('form[onsubmit*="confirm"]');
confirmForms.forEach(form => {
form.onsubmit = function() {
const message = this.getAttribute('onsubmit').match(/confirm\('([^']+)'\)/)[1];
return confirm(message);
};
});
});
</script>
{% endblock %}

View File

@@ -1,503 +1,293 @@
<!DOCTYPE html>
<html lang="de" class="h-full">
<html lang="de" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}MYP - Mercedes 3D Printing Platform{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0">
<meta name="description" content="MYP Platform - Mercedes-Benz 3D Druck Management System">
<meta name="robots" content="noindex, nofollow">
<!-- Dynamic theme-color meta tags for browser UI -->
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="theme-color" id="metaThemeColor" content="#000000">
<!-- CSRF-Token für Formulare -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Lokale Tailwind CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.min.css') }}">
<!-- Title -->
<title>{% block title %}MYP Platform - Mercedes-Benz{% endblock %}</title>
<!-- Custom CSS für Mercedes Farben -->
<!-- PWA Manifest -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='icons/apple-touch-icon.png') }}">
<!-- CSS -->
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<!-- Preload critical resources -->
<link rel="preload" href="{{ url_for('static', filename='js/ui-components.js') }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='js/offline-app.js') }}" as="script">
<!-- Additional CSS -->
{% block extra_css %}{% endblock %}
<!-- Dark Mode Script (must be in head to prevent flash) -->
<script>
// Temporär für sofortige Anwendung ohne Flackern
document.documentElement.classList.add('disable-transitions');
const STORAGE_KEY = 'myp-dark-mode';
const savedMode = localStorage.getItem(STORAGE_KEY);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = savedMode === 'true' || (savedMode === null && prefersDark);
// Dark Mode sofort anwenden
if (isDark) {
document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.style.colorScheme = 'dark';
} else {
document.documentElement.classList.remove('dark');
document.documentElement.setAttribute('data-theme', 'light');
document.documentElement.style.colorScheme = 'light';
}
// ThemeColor Meta-Tag aktualisieren
const metaThemeColor = document.getElementById('metaThemeColor');
if (metaThemeColor) {
metaThemeColor.setAttribute('content', isDark ? '#000000' : '#ffffff');
}
// Übergänge nach kurzer Verzögerung wieder aktivieren
setTimeout(function() {
document.documentElement.classList.remove('disable-transitions');
}, 10);
</script>
<!-- Disable Transitions Styling -->
<style>
:root {
--mercedes-silver: #C0C0C0;
--mercedes-dark-gray: #2D2D2D;
--mercedes-light-gray: #F5F5F5;
--mercedes-blue: #0066CC;
--mercedes-green: #00B04F;
--mercedes-red: #E60012;
--mercedes-yellow: #FFD700;
--mercedes-black: #000000;
--mercedes-white: #FFFFFF;
}
/* Mercedes Color Classes */
.bg-mercedes-silver { background-color: var(--mercedes-silver); }
.bg-mercedes-dark-gray { background-color: var(--mercedes-dark-gray); }
.bg-mercedes-light-gray { background-color: var(--mercedes-light-gray); }
.bg-mercedes-blue { background-color: var(--mercedes-blue); }
.bg-mercedes-green { background-color: var(--mercedes-green); }
.bg-mercedes-red { background-color: var(--mercedes-red); }
.bg-mercedes-yellow { background-color: var(--mercedes-yellow); }
.bg-mercedes-black { background-color: var(--mercedes-black); }
.bg-mercedes-white { background-color: var(--mercedes-white); }
.text-mercedes-silver { color: var(--mercedes-silver); }
.text-mercedes-dark-gray { color: var(--mercedes-dark-gray); }
.text-mercedes-light-gray { color: var(--mercedes-light-gray); }
.text-mercedes-blue { color: var(--mercedes-blue); }
.text-mercedes-green { color: var(--mercedes-green); }
.text-mercedes-red { color: var(--mercedes-red); }
.text-mercedes-yellow { color: var(--mercedes-yellow); }
.text-mercedes-black { color: var(--mercedes-black); }
.text-mercedes-white { color: var(--mercedes-white); }
.border-mercedes-silver { border-color: var(--mercedes-silver); }
.border-mercedes-dark-gray { border-color: var(--mercedes-dark-gray); }
.border-mercedes-light-gray { border-color: var(--mercedes-light-gray); }
.border-mercedes-blue { border-color: var(--mercedes-blue); }
.border-mercedes-green { border-color: var(--mercedes-green); }
.border-mercedes-red { border-color: var(--mercedes-red); }
.border-mercedes-yellow { border-color: var(--mercedes-yellow); }
.border-mercedes-black { border-color: var(--mercedes-black); }
.border-mercedes-white { border-color: var(--mercedes-white); }
/* Mercedes Gradient */
.mercedes-gradient {
background: linear-gradient(135deg, var(--mercedes-black) 0%, var(--mercedes-dark-gray) 100%);
}
/* Mercedes Shadow */
.mercedes-shadow {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.mercedes-shadow-lg {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Mercedes Button */
.mercedes-button {
transition: all 0.2s ease-in-out;
border-radius: 0.5rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.mercedes-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.mercedes-button:active {
transform: translateY(0);
}
/* Mercedes Card */
.mercedes-card {
background: var(--mercedes-white);
border: 1px solid var(--mercedes-light-gray);
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.mercedes-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
/* Mercedes Input */
.mercedes-input {
border: 2px solid var(--mercedes-light-gray);
border-radius: 0.5rem;
padding: 0.75rem 1rem;
transition: all 0.2s ease-in-out;
background: var(--mercedes-white);
}
.mercedes-input:focus {
outline: none;
border-color: var(--mercedes-blue);
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
/* Mercedes Table */
.mercedes-table {
background: var(--mercedes-white);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.mercedes-table th {
background: var(--mercedes-dark-gray);
color: var(--mercedes-white);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 1rem;
}
.mercedes-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--mercedes-light-gray);
}
.mercedes-table tr:hover {
background: var(--mercedes-light-gray);
}
/* Mercedes Modal */
.mercedes-modal {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.mercedes-modal-content {
background: var(--mercedes-white);
border-radius: 1rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
max-height: 90vh;
overflow-y: auto;
}
/* Mercedes Progress Bar */
.mercedes-progress {
background: var(--mercedes-light-gray);
border-radius: 9999px;
overflow: hidden;
}
.mercedes-progress-bar {
background: linear-gradient(90deg, var(--mercedes-blue), var(--mercedes-green));
height: 100%;
transition: width 0.3s ease-in-out;
}
/* Mercedes Status Badges */
.status-online { background: var(--mercedes-green); color: white; }
.status-offline { background: var(--mercedes-red); color: white; }
.status-busy { background: var(--mercedes-yellow); color: var(--mercedes-black); }
.status-maintenance { background: var(--mercedes-silver); color: var(--mercedes-black); }
.status-pending { background: var(--mercedes-yellow); color: var(--mercedes-black); }
.status-printing { background: var(--mercedes-blue); color: white; }
.status-completed { background: var(--mercedes-green); color: white; }
.status-failed { background: var(--mercedes-red); color: white; }
.status-cancelled { background: var(--mercedes-silver); color: var(--mercedes-black); }
/* Mercedes Navigation */
.mercedes-nav-item {
position: relative;
transition: all 0.2s ease-in-out;
}
.mercedes-nav-item:hover::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: var(--mercedes-silver);
}
/* Mercedes Animations */
@keyframes mercedes-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.mercedes-pulse {
animation: mercedes-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes mercedes-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.mercedes-spin {
animation: mercedes-spin 1s linear infinite;
}
/* Mercedes Responsive */
@media (max-width: 768px) {
.mercedes-card {
margin: 0.5rem;
}
.mercedes-table {
font-size: 0.875rem;
}
.mercedes-button {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
}
/* Mercedes Dark Mode Support */
@media (prefers-color-scheme: dark) {
.mercedes-card {
background: var(--mercedes-dark-gray);
border-color: var(--mercedes-silver);
color: var(--mercedes-white);
}
.mercedes-input {
background: var(--mercedes-dark-gray);
color: var(--mercedes-white);
border-color: var(--mercedes-silver);
}
.disable-transitions,
.disable-transitions * {
transition: none !important;
}
</style>
{% block head %}{% endblock %}
</head>
<body class="h-full bg-gradient-to-br from-mercedes-light-gray to-white font-sans">
<body class="min-h-screen flex flex-col bg-white text-slate-900 dark:bg-[#050505] dark:text-slate-200 transition-colors duration-300 text-base mercedes-background">
<!-- Navigation -->
<nav class="mercedes-gradient mercedes-shadow sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Logo und Marke -->
<div class="flex items-center space-x-4">
<div class="flex-shrink-0">
<svg class="h-10 w-10 text-mercedes-silver" fill="currentColor" viewBox="0 0 80 80">
<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 class="text-white">
<h1 class="text-xl font-bold tracking-wide">MYP</h1>
<p class="text-xs text-mercedes-silver">3D Printing Platform</p>
</div>
<nav class="navbar">
<div class="max-w-7xl mx-auto px-2 sm:px-4 lg:px-6">
<div class="flex items-center justify-between h-14 sm:h-16 lg:h-20">
<!-- Brand Section -->
<div class="flex-shrink-0">
<a href="{{ url_for('dashboard') }}" class="navbar-brand" aria-label="Zur Startseite">
<!-- Mercedes-Benz Logo -->
<div class="w-8 h-8 sm:w-10 sm:h-10 lg:w-12 lg:h-12 transition-all duration-300 group-hover:rotate-12">
<svg class="w-full h-full text-slate-900 dark:text-white transition-colors duration-300" 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>
<!-- Brand Text -->
<div class="flex flex-col ml-2">
<span class="text-xl lg:text-2xl font-bold text-slate-900 dark:text-white transition-colors duration-300 tracking-tight">Mercedes-Benz</span>
<span class="text-xs lg:text-sm text-slate-600 dark:text-slate-400 font-medium transition-colors duration-300">MYP 3D-Druck Platform</span>
</div>
</a>
</div>
<!-- Navigation Links -->
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-8">
<a href="/dashboard" class="mercedes-nav-item text-white hover:text-mercedes-silver px-3 py-2 text-sm font-medium transition-colors duration-200">
Dashboard
<!-- Desktop Navigation Menu -->
<div class="hidden lg:flex flex-1 justify-center px-8">
<nav class="navbar-menu" role="navigation" aria-label="Hauptnavigation">
<a href="{{ url_for('dashboard') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/>
</svg>
<span>Dashboard</span>
</a>
<a href="/printers" class="mercedes-nav-item text-white hover:text-mercedes-silver px-3 py-2 text-sm font-medium transition-colors duration-200">
Drucker
<a href="{{ url_for('printers_page') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'printers_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
<span>3D-Drucker</span>
</a>
<a href="/jobs" class="mercedes-nav-item text-white hover:text-mercedes-silver px-3 py-2 text-sm font-medium transition-colors duration-200">
Jobs
<a href="{{ url_for('jobs_page') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'jobs_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 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 2"/>
</svg>
<span>Druckaufträge</span>
</a>
<a href="/stats" class="mercedes-nav-item text-white hover:text-mercedes-silver px-3 py-2 text-sm font-medium transition-colors duration-200">
Statistiken
<a href="{{ url_for('stats_page') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'stats_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
<span>Statistiken</span>
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="/admin" class="mercedes-nav-item text-mercedes-yellow hover:text-white px-3 py-2 text-sm font-medium transition-colors duration-200">
Admin
<a href="{{ url_for('admin_page') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'admin_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>Administration</span>
</a>
{% endif %}
</div>
</nav>
</div>
<!-- User Menu -->
<div class="flex items-center space-x-4">
{% if current_user.is_authenticated %}
<div class="text-white text-sm">
<span class="text-mercedes-silver">Willkommen,</span>
<span class="font-medium">{{ current_user.email }}</span>
</div>
<button onclick="logout()" class="bg-mercedes-red hover:bg-red-700 text-white px-4 py-2 rounded-lg text-sm font-medium mercedes-button transition-all duration-200">
Abmelden
<!-- Right Side Controls -->
<div class="flex items-center space-x-2 sm:space-x-3">
<!-- Dark Mode Toggle -->
<button
id="darkModeToggle"
class="dark-mode-toggle"
aria-label="Dark Mode umschalten"
aria-pressed="false"
data-action="toggle-dark-mode"
title="Dark Mode aktivieren"
>
<svg class="w-4 h-4 sm:w-5 sm:h-5 transition-all duration-300 transform" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
{% if current_user.is_authenticated %}
<!-- User Profile Dropdown -->
<div class="relative" id="user-menu-container">
<button
id="user-menu-button"
class="flex items-center space-x-2 text-sm font-medium rounded-lg p-1.5 transition-all duration-300 hover:bg-blue-50 dark:hover:bg-blue-950"
aria-expanded="false"
aria-haspopup="true"
aria-label="Benutzermenu öffnen"
>
<!-- Profile Avatar -->
<div class="h-8 w-8 rounded-full flex items-center justify-center bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 font-medium transition-colors duration-300">
{{ current_user.email[0].upper() if current_user.email else 'U' }}
</div>
<!-- User Info (hidden on mobile) -->
<div class="hidden md:block text-left ml-2">
<div class="text-sm font-medium text-slate-900 dark:text-white transition-colors duration-300">{{ current_user.email.split('@')[0] if current_user.email else 'Benutzer' }}</div>
<div class="text-xs text-slate-600 dark:text-slate-400 transition-colors duration-300">Mercedes-Benz Mitarbeiter</div>
</div>
</button>
</div>
{% else %}
<a href="/login" class="bg-mercedes-blue hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium mercedes-button transition-all duration-200">
Anmelden
<!-- Login Button -->
<a href="{{ url_for('auth.login') }}"
class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-sm hover:shadow-md transition-all duration-300 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
<span class="hidden sm:inline">Anmelden</span>
</a>
{% endif %}
</div>
<!-- Mobile menu button -->
<div class="md:hidden">
<button type="button" class="text-white hover:text-mercedes-silver focus:outline-none focus:text-mercedes-silver" onclick="toggleMobileMenu()">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<div id="mobile-menu" class="md:hidden hidden">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-mercedes-gray">
<a href="/dashboard" class="text-white hover:text-mercedes-silver block px-3 py-2 text-base font-medium">Dashboard</a>
<a href="/printers" class="text-white hover:text-mercedes-silver block px-3 py-2 text-base font-medium">Drucker</a>
<a href="/jobs" class="text-white hover:text-mercedes-silver block px-3 py-2 text-base font-medium">Jobs</a>
<a href="/stats" class="text-white hover:text-mercedes-silver block px-3 py-2 text-base font-medium">Statistiken</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="/admin" class="text-mercedes-yellow hover:text-white block px-3 py-2 text-base font-medium">Admin</a>
{% endif %}
</div>
</div>
</nav>
<!-- Flash Messages -->
<div id="flash-messages" class="fixed top-20 right-4 z-40 space-y-2">
<!-- Flash messages will be inserted here by JavaScript -->
</div>
<!-- Main Content -->
<main class="min-h-screen">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="mercedes-gradient text-white py-8 mt-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="flex items-center space-x-4 mb-4 md:mb-0">
<svg class="h-8 w-8 text-mercedes-silver" fill="currentColor" viewBox="0 0 80 80">
<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>
<p class="text-sm font-medium">MYP - 3D Printing Platform</p>
<p class="text-xs text-mercedes-silver">Powered by Mercedes Excellence</p>
<main id="main-content" class="flex-grow max-w-7xl w-full mx-auto px-3 sm:px-6 lg:px-8 py-4 sm:py-8">
<div class="container mx-auto px-4 py-8">
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages mb-4">
{% for category, message in messages %}
<div class="alert alert-{{ category }} mb-2">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
<!-- Mercedes-Benz Footer -->
<footer class="bg-white dark:bg-black border-t border-gray-200 dark:border-slate-700 mt-auto transition-colors duration-300">
<div class="max-w-screen-xl w-full mx-auto px-3 sm:px-6 lg:px-8">
<div class="py-4 sm:py-8 border-t border-gray-200 dark:border-slate-700 mt-8 sm:mt-12 pt-4 sm:pt-8">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6 sm:gap-8">
<!-- Brand Section - Stack on mobile -->
<div class="flex flex-col space-y-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8">
<svg class="w-full h-full text-slate-900 dark:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 80 80">
<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>
<div class="text-lg font-bold text-slate-900 dark:text-white transition-colors duration-300">Mercedes-Benz</div>
<div class="text-sm text-slate-600 dark:text-slate-400 transition-colors duration-300">MYP Platform</div>
</div>
</div>
<p class="text-xs text-slate-600 dark:text-slate-400 leading-relaxed transition-colors duration-300">
Das Beste oder nichts - Professionelles 3D-Druck Management für Mercedes-Benz.
</p>
</div>
<!-- System Info -->
<div class="flex flex-col space-y-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white transition-colors duration-300">System</h3>
<div class="space-y-2 text-xs text-slate-600 dark:text-slate-400 leading-relaxed transition-colors duration-300">
<div class="flex justify-between">
<span>Version:</span>
<span class="text-slate-900 dark:text-white font-medium transition-colors duration-300">3.0.0</span>
</div>
<div class="flex justify-between">
<span>Status:</span>
<div id="connection-status" class="flex items-center space-x-1">
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span class="text-green-500 dark:text-green-400 font-medium transition-colors duration-300">Online</span>
</div>
</div>
</div>
</div>
<!-- Copyright -->
<div class="flex flex-col space-y-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white transition-colors duration-300">Rechtliches</h3>
<div class="space-y-2 text-xs text-slate-600 dark:text-slate-400 leading-relaxed transition-colors duration-300">
<p>© 2024 Mercedes-Benz Group AG</p>
<p>Alle Rechte vorbehalten.</p>
<p class="text-xs text-slate-500 dark:text-slate-500 transition-colors duration-300">
Entwickelt für interne Mercedes-Benz Anwendungen
</p>
</div>
</div>
</div>
<div class="text-center md:text-right">
<p class="text-sm text-mercedes-silver">© 2024 MYP Platform. Alle Rechte vorbehalten.</p>
<p class="text-xs text-mercedes-silver mt-1">Version 1.0</p>
</div>
</div>
</div>
</footer>
<!-- JavaScript -->
<script>
// Mobile menu toggle
function toggleMobileMenu() {
const menu = document.getElementById('mobile-menu');
menu.classList.toggle('hidden');
}
// Logout function
async function logout() {
try {
const response = await fetch('/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
window.location.href = '/login';
} else {
showFlashMessage('Fehler beim Abmelden', 'error');
}
} catch (error) {
showFlashMessage('Netzwerkfehler beim Abmelden', 'error');
}
}
// Flash message system
function showFlashMessage(message, type = 'info') {
const container = document.getElementById('flash-messages');
const messageDiv = document.createElement('div');
let bgColor = 'bg-mercedes-blue';
let textColor = 'text-white';
switch(type) {
case 'success':
bgColor = 'bg-mercedes-green';
break;
case 'error':
bgColor = 'bg-mercedes-red';
break;
case 'warning':
bgColor = 'bg-mercedes-yellow';
textColor = 'text-mercedes-black';
break;
}
messageDiv.className = `${bgColor} ${textColor} px-6 py-3 rounded-lg shadow-lg mercedes-shadow transform transition-all duration-300 translate-x-full`;
messageDiv.innerHTML = `
<div class="flex items-center justify-between">
<span class="font-medium">${message}</span>
<button onclick="this.parentElement.parentElement.remove()" class="ml-4 text-lg font-bold hover:opacity-75">×</button>
</div>
`;
container.appendChild(messageDiv);
// Animate in
setTimeout(() => {
messageDiv.classList.remove('translate-x-full');
}, 100);
// Auto remove after 5 seconds
setTimeout(() => {
messageDiv.classList.add('translate-x-full');
setTimeout(() => {
if (messageDiv.parentElement) {
messageDiv.remove();
}
}, 300);
}, 5000);
}
// API helper function
async function apiCall(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'API-Fehler');
}
return data;
} catch (error) {
showFlashMessage(error.message, 'error');
throw error;
}
}
// Format date helper
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// Format duration helper
function formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
} else {
return `${secs}s`;
}
}
</script>
<script src="{{ url_for('static', filename='js/ui-components.js') }}"></script>
{% block scripts %}{% endblock %}
</body>

View File

@@ -1,461 +1,488 @@
{% extends "base.html" %}
{% block title %}Dashboard - MYP Platform{% endblock %}
{% 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 {
@apply bg-white/70 dark:bg-black/80 backdrop-blur-lg border border-gray-200/80 dark:border-slate-700/30 rounded-xl shadow-xl transition-all duration-300;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.dark .glass-card-light {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
.section-title {
@apply text-slate-800 dark:text-white text-xl font-semibold mb-4;
}
.stat-value {
@apply text-slate-900 dark:text-white font-bold text-2xl;
}
.stat-label {
@apply text-slate-500 dark:text-slate-400 text-sm;
}
</style>
{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-mercedes-black">Dashboard</h1>
<p class="mt-2 text-mercedes-gray">Willkommen zurück! Hier ist Ihre Übersicht.</p>
<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 space-x-4">
<button onclick="refreshDashboard()" class="bg-mercedes-blue hover:bg-blue-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
Aktualisieren
<span>Aktualisieren</span>
</button>
<a href="/jobs/new" class="bg-mercedes-green hover:bg-green-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
Neuer Job
<span>Neuer Auftrag</span>
</a>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Active Jobs -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="bg-mercedes-green p-3 rounded-full">
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</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="ml-4">
<p class="text-sm font-medium text-mercedes-gray">Aktive Jobs</p>
<p class="text-2xl font-bold text-mercedes-black" id="active-jobs-count">-</p>
<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>
<!-- Scheduled Jobs -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="bg-mercedes-blue p-3 rounded-full">
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
</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="ml-4">
<p class="text-sm font-medium text-mercedes-gray">Geplante Jobs</p>
<p class="text-2xl font-bold text-mercedes-black" id="scheduled-jobs-count">-</p>
<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>
<!-- Available Printers -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="bg-mercedes-yellow p-3 rounded-full">
<svg class="h-6 w-6 text-mercedes-black" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</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="ml-4">
<p class="text-sm font-medium text-mercedes-gray">Verfügbare Drucker</p>
<p class="text-2xl font-bold text-mercedes-black" id="available-printers-count">-</p>
<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>
<!-- Total Print Time -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="bg-mercedes-silver p-3 rounded-full">
<svg class="h-6 w-6 text-mercedes-black" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</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="ml-4">
<p class="text-sm font-medium text-mercedes-gray">Gesamte Druckzeit</p>
<p class="text-2xl font-bold text-mercedes-black" id="total-print-time">-</p>
<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>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Recent Jobs -->
<div class="lg:col-span-2">
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">Aktuelle Jobs</h2>
<a href="/jobs" class="text-mercedes-blue hover:text-blue-700 text-sm font-medium transition-colors duration-200">
Alle anzeigen →
</a>
</div>
<div id="recent-jobs" class="space-y-4">
<!-- Jobs will be loaded here -->
<div class="text-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-mercedes-blue mx-auto"></div>
<p class="mt-2 text-mercedes-gray">Lade Jobs...</p>
<!-- 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>
</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>
<!-- Quick Actions & System Status -->
<div class="space-y-6">
<!-- Quick Actions -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<h2 class="text-xl font-bold text-mercedes-black mb-6">Schnellaktionen</h2>
<div class="space-y-4">
<a href="/jobs/new" class="block w-full bg-mercedes-green hover:bg-green-700 text-white text-center py-3 px-4 rounded-lg mercedes-button transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" 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>
Neuen Job erstellen
</a>
<a href="/printers" class="block w-full bg-mercedes-blue hover:bg-blue-700 text-white text-center py-3 px-4 rounded-lg mercedes-button transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
Drucker verwalten
</a>
<a href="/stats" class="block w-full bg-mercedes-silver hover:bg-gray-400 text-mercedes-black text-center py-3 px-4 rounded-lg mercedes-button transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Statistiken anzeigen
</a>
</div>
</div>
<!-- System Status -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<h2 class="text-xl font-bold text-mercedes-black mb-6">Systemstatus</h2>
<div class="space-y-4">
<!-- Scheduler Status -->
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-mercedes-gray">Job-Scheduler</span>
<div class="flex items-center">
<div id="scheduler-status" class="h-3 w-3 rounded-full bg-mercedes-gray mr-2"></div>
<span id="scheduler-text" class="text-sm text-mercedes-gray">Prüfe...</span>
<!-- 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>
<!-- Database Status -->
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-mercedes-gray">Datenbank</span>
<div class="flex items-center">
<div class="h-3 w-3 rounded-full bg-mercedes-green mr-2"></div>
<span class="text-sm text-mercedes-green">Online</span>
</div>
</div>
<!-- API Status -->
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-mercedes-gray">API</span>
<div class="flex items-center">
<div class="h-3 w-3 rounded-full bg-mercedes-green mr-2"></div>
<span class="text-sm text-mercedes-green">Verfügbar</span>
</div>
</div>
</div>
{% if current_user.is_admin %}
<div class="mt-6 pt-4 border-t border-mercedes-silver">
<a href="/admin" class="block w-full bg-mercedes-yellow hover:bg-yellow-500 text-mercedes-black text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200 text-sm font-medium">
<svg class="h-4 w-4 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
{% 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>
Admin-Panel
</a>
</div>
<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>
<!-- Recent Activity -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<h2 class="text-xl font-bold text-mercedes-black mb-6">Letzte Aktivitäten</h2>
<div id="recent-activity" class="space-y-3">
<!-- Activity will be loaded here -->
<div class="text-center py-4">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-mercedes-blue mx-auto"></div>
<p class="mt-2 text-sm text-mercedes-gray">Lade Aktivitäten...</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{% block extra_js %}
<script>
// Dashboard data
let dashboardData = {
stats: {},
jobs: [],
printers: [],
schedulerStatus: false
};
// Load dashboard data
async function loadDashboardData() {
try {
// Load stats
const statsResponse = await apiCall('/api/stats');
dashboardData.stats = statsResponse;
// Load jobs
const jobsResponse = await apiCall('/api/jobs');
dashboardData.jobs = jobsResponse.jobs || [];
// Load printers
const printersResponse = await apiCall('/api/printers');
dashboardData.printers = printersResponse.printers || [];
// Load scheduler status (try to load, will fail if not admin)
try {
const schedulerResponse = await apiCall('/api/scheduler/status');
dashboardData.schedulerStatus = schedulerResponse.running;
} catch (error) {
console.log('Scheduler status not available (not admin or error)');
dashboardData.schedulerStatus = false;
}
updateDashboard();
} catch (error) {
console.error('Error loading dashboard data:', error);
showFlashMessage('Fehler beim Laden der Dashboard-Daten', 'error');
}
}
// Update dashboard display
function updateDashboard() {
updateStats();
updateRecentJobs();
updateSystemStatus();
updateRecentActivity();
}
// Update stats cards
function updateStats() {
const stats = dashboardData.stats;
const jobs = dashboardData.jobs;
const printers = dashboardData.printers;
// Active jobs
const activeJobs = jobs.filter(job => job.status === 'active').length;
document.getElementById('active-jobs-count').textContent = activeJobs;
// Scheduled jobs
const scheduledJobs = jobs.filter(job => job.status === 'scheduled').length;
document.getElementById('scheduled-jobs-count').textContent = scheduledJobs;
// Available printers
const availablePrinters = printers.filter(printer => printer.status === 'available').length;
document.getElementById('available-printers-count').textContent = availablePrinters;
// Total print time
const totalTime = stats.total_print_time || 0;
document.getElementById('total-print-time').textContent = formatDuration(totalTime);
}
// Update recent jobs
function updateRecentJobs() {
const container = document.getElementById('recent-jobs');
const recentJobs = dashboardData.jobs
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.slice(0, 5);
if (recentJobs.length === 0) {
container.innerHTML = `
<div class="text-center py-8">
<svg class="h-12 w-12 text-mercedes-silver mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 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 2" />
</svg>
<p class="text-mercedes-gray">Noch keine Jobs vorhanden</p>
<a href="/jobs/new" class="mt-2 inline-block text-mercedes-blue hover:text-blue-700 font-medium">Ersten Job erstellen</a>
</div>
`;
return;
}
container.innerHTML = recentJobs.map(job => {
const statusColor = getJobStatusColor(job.status);
const statusText = getJobStatusText(job.status);
return `
<div class="border border-mercedes-silver rounded-lg p-4 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="font-medium text-mercedes-black">${job.title}</h3>
<p class="text-sm text-mercedes-gray mt-1">
Drucker: ${job.printer_name || 'Unbekannt'}
Erstellt: ${formatDate(job.created_at)}
</p>
</div>
<div class="ml-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}">
${statusText}
</span>
</div>
</div>
<div class="mt-3 flex items-center justify-between">
<div class="text-sm text-mercedes-gray">
${job.start_time ? `Start: ${formatDate(job.start_time)}` : 'Kein Startzeit'}
</div>
<a href="/jobs/${job.id}" class="text-mercedes-blue hover:text-blue-700 text-sm font-medium">
Details →
</a>
</div>
</div>
`;
}).join('');
}
// Update system status
function updateSystemStatus() {
const schedulerStatus = document.getElementById('scheduler-status');
const schedulerText = document.getElementById('scheduler-text');
if (dashboardData.schedulerStatus) {
schedulerStatus.className = 'h-3 w-3 rounded-full bg-mercedes-green mr-2';
schedulerText.textContent = 'Aktiv';
schedulerText.className = 'text-sm text-mercedes-green';
} else {
schedulerStatus.className = 'h-3 w-3 rounded-full bg-mercedes-red mr-2';
schedulerText.textContent = 'Inaktiv';
schedulerText.className = 'text-sm text-mercedes-red';
}
}
// Update recent activity
function updateRecentActivity() {
const container = document.getElementById('recent-activity');
const activities = [];
// Generate activity from recent jobs
dashboardData.jobs
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.slice(0, 5)
.forEach(job => {
activities.push({
type: 'job_created',
message: `Job "${job.title}" erstellt`,
time: job.created_at,
icon: 'plus'
});
if (job.status === 'active') {
activities.push({
type: 'job_started',
message: `Job "${job.title}" gestartet`,
time: job.start_time,
icon: 'play'
});
}
});
// Sort by time
activities.sort((a, b) => new Date(b.time) - new Date(a.time));
if (activities.length === 0) {
container.innerHTML = `
<div class="text-center py-4">
<p class="text-sm text-mercedes-gray">Keine Aktivitäten</p>
</div>
`;
return;
}
container.innerHTML = activities.slice(0, 5).map(activity => {
const icon = getActivityIcon(activity.icon);
return `
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<div class="bg-mercedes-blue p-1 rounded-full">
${icon}
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm text-mercedes-black">${activity.message}</p>
<p class="text-xs text-mercedes-gray">${formatDate(activity.time)}</p>
</div>
</div>
`;
}).join('');
}
// Helper functions
function getJobStatusColor(status) {
switch (status) {
case 'active': return 'bg-mercedes-green text-white';
case 'scheduled': return 'bg-mercedes-blue text-white';
case 'completed': return 'bg-mercedes-silver text-mercedes-black';
case 'aborted': return 'bg-mercedes-red text-white';
default: return 'bg-mercedes-gray text-white';
}
}
function getJobStatusText(status) {
switch (status) {
case 'active': return 'Aktiv';
case 'scheduled': return 'Geplant';
case 'completed': return 'Abgeschlossen';
case 'aborted': return 'Abgebrochen';
default: return 'Unbekannt';
}
}
function getActivityIcon(type) {
switch (type) {
case 'plus':
return '<svg class="h-4 w-4 text-white" 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>';
case 'play':
return '<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1.586a1 1 0 01.707.293l2.414 2.414a1 1 0 00.707.293H15" /></svg>';
default:
return '<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>';
}
}
// Refresh dashboard
function refreshDashboard() {
showFlashMessage('Dashboard wird aktualisiert...', 'info');
loadDashboardData();
}
// Initialize dashboard
document.addEventListener('DOMContentLoaded', function() {
loadDashboardData();
// Auto-refresh every 30 seconds
setInterval(loadDashboardData, 30000);
// Refresh Button
const refreshBtn = document.getElementById('refreshDashboard');
if (refreshBtn) {
refreshBtn.addEventListener('click', function() {
window.location.reload();
});
}
// Dashboard Theme Switch Updates
window.addEventListener('darkModeChanged', function(e) {
// Update any theme-specific elements if needed
console.log('Theme changed to:', e.detail.isDark ? 'dark' : 'light');
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block content %}
<div class="space-y-6">
<header class="mb-6">
<h1 class="text-3xl font-bold text-white mb-2">MYP Platform</h1>
<p class="text-gray-300">3D-Drucker Reservierungssystem für Mercedes-Benz</p>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Quick Stats -->
<div class="bg-slate-800/70 rounded-2xl p-6 shadow-md">
<h2 class="text-xl font-semibold text-white mb-4">Aktive Druckaufträge</h2>
<div data-api-endpoint="/api/jobs/active" data-counter data-title="Laufende Druckaufträge"></div>
</div>
<div class="bg-slate-800/70 rounded-2xl p-6 shadow-md">
<h2 class="text-xl font-semibold text-white mb-4">Verfügbare Drucker</h2>
<div data-api-endpoint="/api/printers" data-counter data-title="Verfügbare Drucker"></div>
</div>
<div class="bg-slate-800/70 rounded-2xl p-6 shadow-md">
<h2 class="text-xl font-semibold text-white mb-4">Schnellzugriff</h2>
<div class="space-y-3">
<a href="{{ url_for('jobs_page') }}" class="flex items-center p-3 bg-slate-700/40 rounded-xl text-white hover:bg-indigo-600/20 transition-colors">
<svg class="w-5 h-5 mr-3" 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 2v10a2 2 0 002 2h8a2 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 2"/>
</svg>
Druckaufträge anzeigen
</a>
<a href="{{ url_for('printers_page') }}" class="flex items-center p-3 bg-slate-700/40 rounded-xl text-white hover:bg-indigo-600/20 transition-colors">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
3D-Drucker verwalten
</a>
</div>
</div>
</div>
<!-- Redirect to dashboard -->
<script>
window.location.href = "{{ url_for('dashboard') }}";
</script>
</div>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,384 +1,412 @@
{% extends "base.html" %}
{% block title %}Anmelden - MYP Platform{% endblock %}
{% block title %}Anmeldung - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<!-- Logo und Header -->
<div class="text-center">
<div class="flex justify-center">
<svg class="h-20 w-20 text-mercedes-black" fill="currentColor" viewBox="0 0 80 80">
<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>
<h2 class="mt-6 text-3xl font-bold text-mercedes-black">
Willkommen bei MYP
</h2>
<p class="mt-2 text-sm text-mercedes-gray">
3D Printing Platform - Powered by Mercedes Excellence
</p>
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center p-4 transition-all duration-500">
<!-- Floating Background Elements -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<!-- Animated Mercedes Stars -->
<div class="absolute top-1/4 left-1/4 w-16 h-16 opacity-5 dark:opacity-10 animate-pulse">
<svg class="w-full h-full text-gray-600 dark:text-gray-400" fill="currentColor" viewBox="0 0 80 80">
<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 class="absolute top-3/4 right-1/4 w-12 h-12 opacity-5 dark:opacity-10 animate-pulse" style="animation-delay: 2s;">
<svg class="w-full h-full text-gray-600 dark:text-gray-400" fill="currentColor" viewBox="0 0 80 80">
<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 class="absolute top-1/2 right-1/3 w-8 h-8 opacity-5 dark:opacity-10 animate-pulse" style="animation-delay: 4s;">
<svg class="w-full h-full text-gray-600 dark:text-gray-400" fill="currentColor" viewBox="0 0 80 80">
<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>
<!-- Decorative Lines -->
<div class="absolute inset-0 opacity-5 dark:opacity-10">
<div class="absolute top-1/3 left-0 right-0 h-px bg-gradient-to-r from-transparent via-gray-600 dark:via-gray-400 to-transparent"></div>
<div class="absolute top-2/3 left-0 right-0 h-px bg-gradient-to-r from-transparent via-gray-600 dark:via-gray-400 to-transparent"></div>
</div>
</div>
<!-- Login Form -->
<div class="mercedes-card rounded-xl shadow-2xl p-8">
<form id="loginForm" class="space-y-6" onsubmit="handleLogin(event)">
<div>
<label for="email" class="block text-sm font-medium text-mercedes-black mb-2">
E-Mail-Adresse
</label>
<input
id="email"
name="email"
type="email"
required
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray"
placeholder="ihre.email@beispiel.de"
>
<!-- Login Card -->
<div class="w-full max-w-md relative z-10">
<div class="bg-white dark:bg-gray-800 backdrop-blur-xl bg-opacity-95 dark:bg-opacity-95 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 p-8 transition-all duration-300 transform hover:shadow-3xl">
<!-- Mercedes-Benz Header -->
<div class="text-center mb-8">
<div class="flex justify-center mb-6">
<div class="w-20 h-20 transition-transform duration-500 hover:scale-110 hover:rotate-12">
<svg class="w-full h-full text-gray-900 dark:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 80 80">
<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-gray-900 dark:text-white mb-1 tracking-tight transition-colors duration-300">Mercedes-Benz</h1>
<div class="w-16 h-0.5 bg-gray-300 dark:bg-gray-600 mx-auto my-3"></div>
<h2 class="text-xl font-medium text-gray-700 dark:text-gray-300 mb-1 transition-colors duration-300">MYP Platform</h2>
<p class="text-gray-600 dark:text-gray-400 text-sm transition-colors duration-300">3D-Druck Management System</p>
</div>
<div>
<label for="password" class="block text-sm font-medium text-mercedes-black mb-2">
Passwort
</label>
<div class="relative">
<input
id="password"
name="password"
type="password"
required
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray pr-12"
placeholder="••••••••"
>
<button
type="button"
onclick="togglePassword()"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-mercedes-gray hover:text-mercedes-black transition-colors duration-200"
>
<svg id="eye-icon" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-6 p-4 {% if category == 'error' %}bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800{% else %}bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800{% endif %} rounded-lg transition-all duration-300">
<div class="flex">
<div class="flex-shrink-0">
{% if category == 'error' %}
<svg class="h-5 w-5 text-red-400" 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>
{% else %}
<svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{% endif %}
</div>
<div class="ml-3">
<h3 class="text-sm font-medium {% if category == 'error' %}text-red-800 dark:text-red-200{% else %}text-blue-800 dark:text-blue-200{% endif %}">{{ category|capitalize }}</h3>
<div class="mt-1 text-sm {% if category == 'error' %}text-red-700 dark:text-red-300{% else %}text-blue-700 dark:text-blue-300{% endif %}">
{{ message }}
</div>
</div>
</div>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Login Form -->
<form id="login-form" action="/auth/login" method="POST" class="space-y-6">
<!-- CSRF-Token hinzufügen -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="space-y-4">
<div>
<label for="username" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2 transition-colors duration-300">
Benutzername
</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500 group-focus-within:text-blue-500 dark:group-focus-within:text-blue-400 transition-colors duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"/>
</svg>
</div>
<input
type="text"
id="username"
name="username"
required
class="block w-full pl-10 pr-3 py-3 border-2 border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300"
placeholder="Benutzername oder E-Mail"
autocomplete="username"
>
</div>
</div>
<div>
<label for="password" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2 transition-colors duration-300">
Passwort
</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500 group-focus-within:text-blue-500 dark:group-focus-within:text-blue-400 transition-colors duration-300" 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 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
<input
type="password"
id="password"
name="password"
required
class="block w-full pl-10 pr-3 py-3 border-2 border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300"
placeholder="••••••••"
autocomplete="current-password"
>
</div>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input
id="remember"
name="remember"
id="remember-me"
name="remember-me"
type="checkbox"
class="h-4 w-4 text-mercedes-blue focus:ring-mercedes-blue border-mercedes-silver rounded"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 dark:border-gray-600 rounded transition-colors duration-300"
>
<label for="remember" class="ml-2 block text-sm text-mercedes-gray">
<label for="remember-me" class="ml-2 block text-sm text-gray-700 dark:text-gray-300 transition-colors duration-300">
Angemeldet bleiben
</label>
</div>
</div>
<div>
<button
type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-mercedes-black hover:bg-mercedes-gray focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-mercedes-blue mercedes-button transition-all duration-200"
id="loginButton"
>
<span id="loginButtonText">Anmelden</span>
<svg id="loginSpinner" class="hidden animate-spin -mr-1 ml-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" 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>
</button>
</div>
</form>
<!-- Divider -->
<div class="mt-6">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-mercedes-silver"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-mercedes-gray">Oder</span>
<div class="text-sm">
<a href="#" class="font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors duration-300">
Passwort vergessen?
</a>
</div>
</div>
</div>
<!-- Register Link -->
<div class="mt-6 text-center">
<p class="text-sm text-mercedes-gray">
Noch kein Konto?
<button
onclick="showRegisterForm()"
class="font-medium text-mercedes-blue hover:text-blue-700 transition-colors duration-200"
>
Jetzt registrieren
</button>
</p>
</div>
</div>
<!-- Register Form (Hidden by default) -->
<div id="registerCard" class="mercedes-card rounded-xl shadow-2xl p-8 hidden">
<div class="text-center mb-6">
<h3 class="text-2xl font-bold text-mercedes-black">Registrierung</h3>
<p class="text-sm text-mercedes-gray mt-2">Erstellen Sie Ihr MYP-Konto</p>
</div>
<form id="registerForm" class="space-y-6" onsubmit="handleRegister(event)">
<div>
<label for="reg-email" class="block text-sm font-medium text-mercedes-black mb-2">
E-Mail-Adresse
</label>
<input
id="reg-email"
name="email"
type="email"
required
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray"
placeholder="ihre.email@beispiel.de"
>
</div>
<div>
<label for="reg-password" class="block text-sm font-medium text-mercedes-black mb-2">
Passwort
</label>
<input
id="reg-password"
name="password"
type="password"
required
minlength="6"
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray"
placeholder="Mindestens 6 Zeichen"
>
</div>
<div>
<label for="reg-password-confirm" class="block text-sm font-medium text-mercedes-black mb-2">
Passwort bestätigen
</label>
<input
id="reg-password-confirm"
name="password_confirm"
type="password"
required
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray"
placeholder="Passwort wiederholen"
>
</div>
<div>
<button
type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-mercedes-green hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-mercedes-green mercedes-button transition-all duration-200"
id="registerButton"
>
<span id="registerButtonText">Registrieren</span>
<svg id="registerSpinner" class="hidden animate-spin -mr-1 ml-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" 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>
</button>
</div>
</form>
<div class="mt-6 text-center">
<button
onclick="showLoginForm()"
class="text-sm font-medium text-mercedes-blue hover:text-blue-700 transition-colors duration-200"
type="submit"
class="w-full flex justify-center items-center py-3 px-4 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 border border-transparent rounded-lg shadow-md text-sm font-semibold text-white hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 transform hover:-translate-y-0.5"
id="login-button"
>
← Zurück zur Anmeldung
<span id="login-text">Anmelden</span>
<svg id="login-spinner" class="hidden animate-spin ml-3 h-5 w-5" 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>
</button>
</form>
<!-- Error Display -->
<div id="error-message" class="hidden mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg transition-all duration-300">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" 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>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Anmeldung fehlgeschlagen</h3>
<div class="mt-2 text-sm text-red-700 dark:text-red-300" id="error-text"></div>
</div>
</div>
</div>
<!-- Offline Notice -->
<div id="offline-notice" class="hidden mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg transition-all duration-300">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" 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>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">Offline-Modus</h3>
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
Sie sind derzeit offline. Die Anwendung läuft im eingeschränkten Modus.
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 transition-colors duration-300">
<div class="text-center">
<p class="text-xs text-gray-500 dark:text-gray-400 transition-colors duration-300">
© 2024 Mercedes-Benz Group AG
</p>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1 transition-colors duration-300">
Das Beste oder nichts
</p>
</div>
</div>
</div>
</div>
<!-- Features -->
<div class="mt-12 grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center">
<div class="flex justify-center">
<div class="bg-mercedes-blue p-3 rounded-full">
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
</div>
<h3 class="mt-4 text-sm font-medium text-mercedes-black">Sicher</h3>
<p class="mt-2 text-xs text-mercedes-gray">Ihre Daten sind bei uns sicher</p>
</div>
<div class="text-center">
<div class="flex justify-center">
<div class="bg-mercedes-green p-3 rounded-full">
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
<h3 class="mt-4 text-sm font-medium text-mercedes-black">Schnell</h3>
<p class="mt-2 text-xs text-mercedes-gray">Blitzschnelle Druckaufträge</p>
</div>
<div class="text-center">
<div class="flex justify-center">
<div class="bg-mercedes-yellow p-3 rounded-full">
<svg class="h-6 w-6 text-mercedes-black" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<h3 class="mt-4 text-sm font-medium text-mercedes-black">Zuverlässig</h3>
<p class="mt-2 text-xs text-mercedes-gray">Mercedes Qualitätsstandards</p>
</div>
<!-- Connection Status Indicator -->
<div id="connection-status-login" class="fixed bottom-6 right-6 z-50 bg-white dark:bg-gray-800 backdrop-blur-lg bg-opacity-90 dark:bg-opacity-90 border border-gray-200 dark:border-gray-700 rounded-full px-4 py-2 shadow-lg transition-all duration-300">
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span class="text-xs font-medium text-gray-700 dark:text-gray-300 transition-colors duration-300">Online</span>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{% block extra_js %}
<script>
// Toggle password visibility
function togglePassword() {
const passwordInput = document.getElementById('password');
const eyeIcon = document.getElementById('eye-icon');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
eyeIcon.innerHTML = `
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
`;
} else {
passwordInput.type = 'password';
eyeIcon.innerHTML = `
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
`;
}
}
document.addEventListener('DOMContentLoaded', function() {
const loginForm = document.getElementById('login-form');
const loginButton = document.getElementById('login-button');
const loginText = document.getElementById('login-text');
const loginSpinner = document.getElementById('login-spinner');
const errorMessage = document.getElementById('error-message');
const errorText = document.getElementById('error-text');
const offlineNotice = document.getElementById('offline-notice');
// Show register form
function showRegisterForm() {
document.querySelector('.mercedes-card').classList.add('hidden');
document.getElementById('registerCard').classList.remove('hidden');
}
// Show login form
function showLoginForm() {
document.getElementById('registerCard').classList.add('hidden');
document.querySelector('.mercedes-card').classList.remove('hidden');
}
// Handle login
async function handleLogin(event) {
event.preventDefault();
const button = document.getElementById('loginButton');
const buttonText = document.getElementById('loginButtonText');
const spinner = document.getElementById('loginSpinner');
// Handle form submission
loginForm.addEventListener('submit', async function(e) {
e.preventDefault();
// Show loading state
button.disabled = true;
buttonText.textContent = 'Anmelden...';
spinner.classList.remove('hidden');
setLoadingState(true);
hideError();
// Offline-Hinweis anzeigen, aber Anmeldung trotzdem erlauben
if (!navigator.onLine) {
showOfflineNotice();
} else {
hideOfflineNotice();
}
const formData = new FormData(loginForm);
const username = formData.get('username');
const password = formData.get('password');
const rememberMe = formData.get('remember-me') === 'on';
try {
const formData = new FormData(event.target);
const response = await fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
email: formData.get('email'),
password: formData.get('password'),
remember: formData.get('remember') === 'on'
username: username,
password: password,
remember_me: rememberMe
})
});
const data = await response.json();
if (response.ok) {
showFlashMessage('Erfolgreich angemeldet!', 'success');
setTimeout(() => {
window.location.href = '/dashboard';
}, 1000);
// Prüfen, ob die Antwort JSON ist
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
if (response.ok) {
// Erfolg - zur Zielseite weiterleiten
loginText.textContent = 'Erfolgreich angemeldet...';
setTimeout(() => {
window.location.href = data.redirect_url || '/dashboard';
}, 1000);
} else {
// Fehler - Fehlermeldung anzeigen
showError(data.error || 'Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Eingaben.');
setLoadingState(false);
}
} else if (response.redirected) {
// Erfolg mit Umleitung - Umleitung folgen
window.location.href = response.url;
} else {
showFlashMessage(data.error || 'Anmeldung fehlgeschlagen', 'error');
// Unerwartete Antwort
console.error('Unerwartete Serverantwort:', await response.text());
showError('Unerwartete Serverantwort. Bitte versuchen Sie es später erneut.');
setLoadingState(false);
}
} catch (error) {
showFlashMessage('Netzwerkfehler bei der Anmeldung', 'error');
} finally {
// Reset button state
button.disabled = false;
buttonText.textContent = 'Anmelden';
spinner.classList.add('hidden');
console.error('Login error:', error);
// Im Falle eines Netzwerkfehlers (wahrscheinlich offline)
showError('Verbindungsfehler. Stellen Sie sicher, dass der Server läuft und erreichbar ist.');
setLoadingState(false);
}
}
// Handle registration
async function handleRegister(event) {
event.preventDefault();
const button = document.getElementById('registerButton');
const buttonText = document.getElementById('registerButtonText');
const spinner = document.getElementById('registerSpinner');
const formData = new FormData(event.target);
const password = formData.get('password');
const passwordConfirm = formData.get('password_confirm');
// Validate passwords match
if (password !== passwordConfirm) {
showFlashMessage('Passwörter stimmen nicht überein', 'error');
return;
}
// Show loading state
button.disabled = true;
buttonText.textContent = 'Registrieren...';
spinner.classList.remove('hidden');
try {
const response = await fetch('/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: formData.get('email'),
password: password
})
});
const data = await response.json();
if (response.ok) {
showFlashMessage('Registrierung erfolgreich! Sie können sich jetzt anmelden.', 'success');
setTimeout(() => {
showLoginForm();
}, 2000);
} else {
showFlashMessage(data.error || 'Registrierung fehlgeschlagen', 'error');
}
} catch (error) {
showFlashMessage('Netzwerkfehler bei der Registrierung', 'error');
} finally {
// Reset button state
button.disabled = false;
buttonText.textContent = 'Registrieren';
spinner.classList.add('hidden');
}
}
// Auto-focus email field on page load
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('email').focus();
});
function setLoadingState(loading) {
loginButton.disabled = loading;
if (loading) {
loginText.textContent = 'Anmeldung läuft...';
loginSpinner.classList.remove('hidden');
} else {
loginText.textContent = 'Anmelden';
loginSpinner.classList.add('hidden');
}
}
function showError(message) {
errorText.textContent = message;
errorMessage.classList.remove('hidden');
errorMessage.classList.add('animate-pulse');
setTimeout(() => {
errorMessage.classList.remove('animate-pulse');
}, 1000);
}
function hideError() {
errorMessage.classList.add('hidden');
}
function showOfflineNotice() {
offlineNotice.classList.remove('hidden');
offlineNotice.classList.add('animate-pulse');
setTimeout(() => {
offlineNotice.classList.remove('animate-pulse');
}, 1000);
}
function hideOfflineNotice() {
offlineNotice.classList.add('hidden');
}
// Connection status monitoring
function updateConnectionStatus() {
const statusEl = document.getElementById('connection-status-login');
if (navigator.onLine) {
statusEl.innerHTML = `
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span class="text-xs font-medium text-gray-700 dark:text-gray-300 transition-colors duration-300">Online</span>
</div>
`;
statusEl.className = 'fixed bottom-6 right-6 z-50 bg-white dark:bg-gray-800 backdrop-blur-lg bg-opacity-90 dark:bg-opacity-90 border border-gray-200 dark:border-gray-700 rounded-full px-4 py-2 shadow-lg transition-all duration-300';
hideOfflineNotice();
} else {
statusEl.innerHTML = `
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-yellow-500 rounded-full animate-pulse"></div>
<span class="text-xs font-medium text-gray-700 dark:text-gray-300 transition-colors duration-300">Offline-Modus</span>
</div>
`;
statusEl.className = 'fixed bottom-6 right-6 z-50 bg-yellow-50 dark:bg-yellow-900 backdrop-blur-lg bg-opacity-90 dark:bg-opacity-90 border border-yellow-200 dark:border-yellow-700 rounded-full px-4 py-2 shadow-lg transition-all duration-300';
showOfflineNotice();
}
}
// Listen for connection changes
window.addEventListener('online', updateConnectionStatus);
window.addEventListener('offline', updateConnectionStatus);
// Initial status
updateConnectionStatus();
// Auto-focus email field
document.getElementById('username').focus();
// Enhanced form validation with visual feedback
const inputs = document.querySelectorAll('input[type="text"], input[type="password"]');
inputs.forEach(input => {
input.addEventListener('blur', function() {
if (this.value && this.checkValidity()) {
this.classList.remove('border-red-300', 'dark:border-red-600');
this.classList.add('border-green-300', 'dark:border-green-600');
} else if (this.value && !this.checkValidity()) {
this.classList.remove('border-green-300', 'dark:border-green-600');
this.classList.add('border-red-300', 'dark:border-red-600');
}
});
input.addEventListener('focus', function() {
this.classList.remove('border-red-300', 'dark:border-red-600', 'border-green-300', 'dark:border-green-600');
});
});
// Add subtle entrance animation
setTimeout(() => {
document.querySelector('.w-full.max-w-md').classList.add('animate-fade-in');
}, 100);
});
</script>
{% endblock %}

View File

@@ -3,24 +3,24 @@
{% block title %}Drucker - MYP Platform{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 py-4 sm:py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-mercedes-black">Drucker</h1>
<p class="mt-2 text-mercedes-gray">Verwalten Sie Ihre 3D-Drucker</p>
<div class="mb-4 sm:mb-8">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div class="mb-4 sm:mb-0">
<h1 class="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white">Drucker</h1>
<p class="mt-1 sm:mt-2 text-sm sm:text-base text-slate-600 dark:text-slate-400">Verwalten Sie Ihre 3D-Drucker</p>
</div>
<div class="flex space-x-4">
<button onclick="refreshPrinters()" class="bg-mercedes-blue hover:bg-blue-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="flex flex-wrap gap-2 sm:space-x-4">
<button onclick="refreshPrinters()" class="flex-1 sm:flex-none bg-indigo-600 hover:bg-indigo-700 dark:bg-indigo-600 dark:hover:bg-indigo-700 text-white px-3 sm:px-4 py-2 rounded-lg transition-all duration-200 text-sm sm:text-base flex items-center justify-center">
<svg class="h-4 w-4 sm:h-5 sm:w-5 mr-1 sm:mr-2" 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>
Aktualisieren
</button>
{% if current_user.is_admin %}
<button onclick="showAddPrinterModal()" class="bg-mercedes-green hover:bg-green-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<button id="addPrinterBtn" class="flex-1 sm:flex-none bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white px-3 sm:px-4 py-2 rounded-lg transition-all duration-200 text-sm sm:text-base flex items-center justify-center">
<svg class="h-4 w-4 sm:h-5 sm:w-5 mr-1 sm:mr-2" 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>
Drucker hinzufügen
@@ -31,11 +31,11 @@
</div>
<!-- Printers Grid -->
<div id="printers-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div id="printers-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-6">
<!-- Loading state -->
<div class="col-span-full text-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-mercedes-blue mx-auto"></div>
<p class="mt-4 text-mercedes-gray">Lade Drucker...</p>
<div class="col-span-full text-center py-6 sm:py-12">
<div class="animate-spin rounded-full h-8 w-8 sm:h-12 sm:w-12 border-b-2 border-indigo-600 dark:border-indigo-400 mx-auto"></div>
<p class="mt-3 sm:mt-4 text-sm sm:text-base text-slate-600 dark:text-slate-400">Lade Drucker...</p>
</div>
</div>
</div>
@@ -44,56 +44,70 @@
{% if current_user.is_admin %}
<div id="addPrinterModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="mercedes-card rounded-xl p-6 w-full max-w-md">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">Neuen Drucker hinzufügen</h2>
<button onclick="hideAddPrinterModal()" class="text-mercedes-gray hover:text-mercedes-black">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="bg-white dark:bg-slate-800 rounded-xl p-4 sm:p-6 w-full max-w-md shadow-lg">
<div class="flex items-center justify-between mb-4 sm:mb-6">
<h2 class="text-lg sm:text-xl font-bold text-slate-900 dark:text-white">Neuen Drucker hinzufügen</h2>
<button id="closeAddPrinterBtn" class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white">
<svg class="h-5 w-5 sm:h-6 sm:w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form id="addPrinterForm" onsubmit="handleAddPrinter(event)" class="space-y-4">
<form id="addPrinterForm" class="space-y-3 sm:space-y-4">
<div>
<label for="printer-name" class="block text-sm font-medium text-mercedes-black mb-2">Name</label>
<label for="printer-name" class="block text-sm font-medium text-slate-900 dark:text-white mb-1 sm:mb-2">Name</label>
<input type="text" id="printer-name" name="name" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:text-white">
</div>
<div>
<label for="printer-model" class="block text-sm font-medium text-mercedes-black mb-2">Modell</label>
<label for="printer-model" class="block text-sm font-medium text-slate-900 dark:text-white mb-1 sm:mb-2">Modell</label>
<input type="text" id="printer-model" name="model" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:text-white">
</div>
<div>
<label for="printer-location" class="block text-sm font-medium text-mercedes-black mb-2">Standort</label>
<label for="printer-location" class="block text-sm font-medium text-slate-900 dark:text-white mb-1 sm:mb-2">Standort</label>
<input type="text" id="printer-location" name="location" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:text-white">
</div>
<div>
<label for="printer-mac" class="block text-sm font-medium text-mercedes-black mb-2">MAC-Adresse</label>
<label for="printer-mac" class="block text-sm font-medium text-slate-900 dark:text-white mb-1 sm:mb-2">MAC-Adresse</label>
<input type="text" id="printer-mac" name="mac_address" required
placeholder="AA:BB:CC:DD:EE:FF"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:text-white">
</div>
<div>
<label for="printer-ip" class="block text-sm font-medium text-mercedes-black mb-2">Plug IP-Adresse</label>
<label for="printer-ip" class="block text-sm font-medium text-slate-900 dark:text-white mb-1 sm:mb-2">Plug IP-Adresse</label>
<input type="text" id="printer-ip" name="plug_ip" required
placeholder="192.168.1.100"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:text-white">
</div>
<div class="flex space-x-3 pt-4">
<button type="button" onclick="hideAddPrinterModal()"
class="flex-1 bg-mercedes-silver hover:bg-gray-400 text-mercedes-black py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
<div>
<label for="printer-plug-username" class="block text-sm font-medium text-slate-900 dark:text-white mb-1 sm:mb-2">Plug Benutzername</label>
<input type="text" id="printer-plug-username" name="plug_username" required
placeholder="admin"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:text-white">
</div>
<div>
<label for="printer-plug-password" class="block text-sm font-medium text-slate-900 dark:text-white mb-1 sm:mb-2">Plug Passwort</label>
<input type="password" id="printer-plug-password" name="plug_password" required
placeholder="••••••••"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:text-white">
</div>
<div class="flex space-x-3 pt-3 sm:pt-4">
<button type="button" id="cancelAddPrinterBtn"
class="flex-1 bg-slate-200 hover:bg-slate-300 text-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600 dark:text-white py-2 px-4 rounded-lg transition-all duration-200 text-sm">
Abbrechen
</button>
<button type="submit"
class="flex-1 bg-mercedes-green hover:bg-green-700 text-white py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
class="flex-1 bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-lg transition-all duration-200 text-sm">
Hinzufügen
</button>
</div>
@@ -106,11 +120,11 @@
<!-- Printer Detail Modal -->
<div id="printerDetailModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="mercedes-card rounded-xl p-6 w-full max-w-lg">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">Drucker Details</h2>
<button onclick="hidePrinterDetailModal()" class="text-mercedes-gray hover:text-mercedes-black">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="bg-white dark:bg-slate-800 rounded-xl p-4 sm:p-6 w-full max-w-lg shadow-lg">
<div class="flex items-center justify-between mb-4 sm:mb-6">
<h2 class="text-lg sm:text-xl font-bold text-slate-900 dark:text-white">Drucker Details</h2>
<button id="closePrinterDetailBtn" class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white">
<svg class="h-5 w-5 sm:h-6 sm:w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
@@ -124,125 +138,12 @@
</div>
{% endblock %}
{% block scripts %}
{% block extra_js %}
<script>
// Globale Variablen für die Drucker-Verwaltung
let printers = [];
// Load printers
async function loadPrinters() {
try {
const response = await apiCall('/api/printers');
printers = response.printers || [];
renderPrinters();
} catch (error) {
console.error('Error loading printers:', error);
showFlashMessage('Fehler beim Laden der Drucker', 'error');
}
}
// Render printers grid
function renderPrinters() {
const grid = document.getElementById('printers-grid');
if (printers.length === 0) {
grid.innerHTML = `
<div class="col-span-full text-center py-12">
<svg class="h-16 w-16 text-mercedes-silver mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
<p class="text-mercedes-gray text-lg">Keine Drucker vorhanden</p>
{% if current_user.is_admin %}
<button onclick="showAddPrinterModal()" class="mt-4 bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
Ersten Drucker hinzufügen
</button>
{% endif %}
</div>
`;
return;
}
grid.innerHTML = printers.map(printer => {
const statusColor = getPrinterStatusColor(printer.status);
const statusText = getPrinterStatusText(printer.status);
return `
<div class="mercedes-card rounded-xl p-6 mercedes-shadow hover:shadow-lg transition-shadow duration-200">
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<h3 class="text-lg font-bold text-mercedes-black">${printer.name}</h3>
<p class="text-sm text-mercedes-gray">${printer.model}</p>
</div>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}">
${statusText}
</span>
</div>
<div class="space-y-2 mb-4">
<div class="flex items-center text-sm text-mercedes-gray">
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
${printer.location}
</div>
<div class="flex items-center text-sm text-mercedes-gray">
<svg class="h-4 w-4 mr-2" 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>
${printer.mac_address}
</div>
<div class="flex items-center text-sm text-mercedes-gray">
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9" />
</svg>
${printer.plug_ip}
</div>
</div>
<div class="flex space-x-2">
<button onclick="showPrinterDetail(${printer.id})"
class="flex-1 bg-mercedes-blue hover:bg-blue-700 text-white py-2 px-3 rounded-lg text-sm mercedes-button transition-all duration-200">
Details
</button>
{% if current_user.is_admin %}
<button onclick="deletePrinter(${printer.id})"
class="bg-mercedes-red hover:bg-red-700 text-white py-2 px-3 rounded-lg text-sm mercedes-button transition-all duration-200">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
{% endif %}
</div>
</div>
`;
}).join('');
}
// Helper functions
function getPrinterStatusColor(status) {
switch (status) {
case 'available': return 'bg-mercedes-green text-white';
case 'busy': return 'bg-mercedes-yellow text-mercedes-black';
case 'offline': return 'bg-mercedes-red text-white';
case 'maintenance': return 'bg-mercedes-silver text-mercedes-black';
default: return 'bg-mercedes-gray text-white';
}
}
function getPrinterStatusText(status) {
switch (status) {
case 'available': return 'Verfügbar';
case 'busy': return 'Beschäftigt';
case 'offline': return 'Offline';
case 'maintenance': return 'Wartung';
default: return 'Unbekannt';
}
}
// Modal functions
// Modal-Funktionen
function showAddPrinterModal() {
document.getElementById('addPrinterModal').classList.remove('hidden');
}
@@ -258,41 +159,58 @@
const content = document.getElementById('printer-detail-content');
content.innerHTML = `
<div class="space-y-4">
<div class="space-y-3 sm:space-y-4">
<div>
<h3 class="font-medium text-mercedes-black">${printer.name}</h3>
<p class="text-sm text-mercedes-gray">${printer.model}</p>
<h3 class="font-medium text-slate-900 dark:text-white text-base sm:text-lg">${printer.name}</h3>
<p class="text-xs sm:text-sm text-slate-600 dark:text-slate-400">${printer.model}</p>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 sm:gap-4 text-xs sm:text-sm">
<div>
<span class="font-medium text-mercedes-gray">Status:</span>
<span class="ml-2 ${getPrinterStatusColor(printer.status)} px-2 py-1 rounded text-xs">
<span class="font-medium text-slate-700 dark:text-slate-300">Status:</span>
<span class="ml-1 sm:ml-2 ${getPrinterStatusColor(printer.status)} px-1.5 sm:px-2 py-0.5 sm:py-1 rounded text-xs">
${getPrinterStatusText(printer.status)}
</span>
</div>
<div>
<span class="font-medium text-mercedes-gray">Standort:</span>
<span class="ml-2 text-mercedes-black">${printer.location}</span>
<span class="font-medium text-slate-700 dark:text-slate-300">Standort:</span>
<span class="ml-1 sm:ml-2 text-slate-900 dark:text-white">${printer.location}</span>
</div>
<div>
<span class="font-medium text-mercedes-gray">MAC:</span>
<span class="ml-2 text-mercedes-black font-mono text-xs">${printer.mac_address}</span>
<span class="font-medium text-slate-700 dark:text-slate-300">MAC:</span>
<span class="ml-1 sm:ml-2 text-slate-900 dark:text-white font-mono text-xs">${printer.mac_address}</span>
</div>
<div>
<span class="font-medium text-mercedes-gray">Plug IP:</span>
<span class="ml-2 text-mercedes-black font-mono text-xs">${printer.plug_ip}</span>
<span class="font-medium text-slate-700 dark:text-slate-300">Plug IP:</span>
<span class="ml-1 sm:ml-2 text-slate-900 dark:text-white font-mono text-xs">${printer.plug_ip}</span>
</div>
</div>
<div class="pt-4 border-t border-mercedes-silver">
<p class="text-xs text-mercedes-gray">
<div class="pt-3 sm:pt-4 border-t border-slate-200 dark:border-slate-700">
<p class="text-xs text-slate-600 dark:text-slate-400">
Erstellt: ${formatDate(printer.created_at)}
</p>
</div>
<div class="flex space-x-3 pt-3 sm:pt-4">
<button onclick="hidePrinterDetailModal()"
class="flex-1 bg-slate-200 hover:bg-slate-300 text-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600 dark:text-white py-2 px-4 rounded-lg transition-all duration-200 text-sm">
Schließen
</button>
{% if current_user.is_admin %}
<button onclick="deletePrinter(${printer.id}); hidePrinterDetailModal();"
class="flex-1 bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-500 text-white py-2 px-4 rounded-lg transition-all duration-200 text-sm flex items-center justify-center">
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Löschen
</button>
{% endif %}
</div>
</div>
`;
@@ -303,30 +221,202 @@
document.getElementById('printerDetailModal').classList.add('hidden');
}
// Load printers
async function loadPrinters() {
try {
const response = await fetch('/api/printers');
if (!response.ok) {
throw new Error('Fehler beim Laden der Drucker');
}
const data = await response.json();
printers = data.printers || [];
renderPrinters();
} catch (error) {
console.error('Error loading printers:', error);
showError('Fehler beim Laden der Drucker');
}
}
// Render printers grid
function renderPrinters() {
const grid = document.getElementById('printers-grid');
if (printers.length === 0) {
grid.innerHTML = `
<div class="col-span-full text-center py-6 sm:py-12">
<svg class="h-12 w-12 sm:h-16 sm:w-16 text-slate-400 dark:text-slate-500 mx-auto mb-3 sm:mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
<p class="text-slate-700 dark:text-slate-300 text-base sm:text-lg">Keine Drucker vorhanden</p>
{% if current_user.is_admin %}
<button id="addFirstPrinterBtn" class="mt-3 sm:mt-4 bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500 text-white px-4 sm:px-6 py-1.5 sm:py-2 rounded-lg transition-all duration-200 text-sm sm:text-base">
Ersten Drucker hinzufügen
</button>
{% endif %}
</div>
`;
// Event-Listener für den "Ersten Drucker hinzufügen" Button
const addFirstPrinterBtn = document.getElementById('addFirstPrinterBtn');
if (addFirstPrinterBtn) {
addFirstPrinterBtn.addEventListener('click', showAddPrinterModal);
}
return;
}
grid.innerHTML = printers.map(printer => {
const statusColor = getPrinterStatusColor(printer.status);
const statusText = getPrinterStatusText(printer.status);
return `
<div class="bg-white dark:bg-slate-800 rounded-xl p-4 sm:p-6 shadow-sm hover:shadow-lg transition-shadow duration-200 border border-slate-200 dark:border-slate-700">
<div class="flex items-start justify-between mb-3 sm:mb-4">
<div class="flex-1">
<h3 class="text-base sm:text-lg font-bold text-slate-900 dark:text-white">${printer.name}</h3>
<p class="text-xs sm:text-sm text-slate-600 dark:text-slate-400">${printer.model}</p>
</div>
<span class="inline-flex items-center px-2 py-0.5 sm:px-2.5 sm:py-0.5 rounded-full text-xs font-medium ${statusColor}">
${statusText}
</span>
</div>
<div class="space-y-1.5 sm:space-y-2 mb-3 sm:mb-4">
<div class="flex items-center text-xs sm:text-sm text-slate-600 dark:text-slate-400">
<svg class="h-3.5 w-3.5 sm:h-4 sm:w-4 mr-1.5 sm:mr-2 text-slate-500 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
${printer.location}
</div>
<div class="flex items-center text-xs sm:text-sm text-slate-600 dark:text-slate-400">
<svg class="h-3.5 w-3.5 sm:h-4 sm:w-4 mr-1.5 sm:mr-2 text-slate-500 dark:text-slate-400" 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>
${printer.mac_address}
</div>
<div class="flex items-center text-xs sm:text-sm text-slate-600 dark:text-slate-400">
<svg class="h-3.5 w-3.5 sm:h-4 sm:w-4 mr-1.5 sm:mr-2 text-slate-500 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9" />
</svg>
${printer.plug_ip}
</div>
</div>
<div class="flex space-x-2">
<button class="printer-detail-btn flex-1 bg-indigo-600 hover:bg-indigo-700 dark:bg-indigo-600 dark:hover:bg-indigo-500 text-white py-1.5 sm:py-2 px-2 sm:px-3 rounded-lg text-xs sm:text-sm transition-all duration-200" data-printer-id="${printer.id}">
Details
</button>
{% if current_user.is_admin %}
<button class="delete-printer-btn flex-1 bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-500 text-white py-1.5 sm:py-2 px-2 sm:px-3 rounded-lg text-xs sm:text-sm transition-all duration-200" data-printer-id="${printer.id}">
<svg class="h-3.5 w-3.5 sm:h-4 sm:w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Löschen
</button>
{% endif %}
</div>
</div>
`;
}).join('');
// Event-Listener für Details und Löschen-Buttons hinzufügen
document.querySelectorAll('.printer-detail-btn').forEach(btn => {
btn.addEventListener('click', function() {
const printerId = parseInt(this.getAttribute('data-printer-id'));
showPrinterDetail(printerId);
});
});
document.querySelectorAll('.delete-printer-btn').forEach(btn => {
btn.addEventListener('click', function() {
const printerId = parseInt(this.getAttribute('data-printer-id'));
deletePrinter(printerId);
});
});
}
// Helper functions
function getPrinterStatusColor(status) {
switch (status) {
case 'available': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
case 'busy': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300';
case 'offline': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
case 'maintenance': return 'bg-slate-200 text-slate-800 dark:bg-slate-700 dark:text-slate-300';
default: return 'bg-slate-200 text-slate-800 dark:bg-slate-700 dark:text-slate-300';
}
}
function getPrinterStatusText(status) {
switch (status) {
case 'available': return 'Verfügbar';
case 'busy': return 'Beschäftigt';
case 'offline': return 'Offline';
case 'maintenance': return 'Wartung';
default: return 'Unbekannt';
}
}
// Format date helper
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('de-DE');
}
// Show error message
function showError(message) {
const grid = document.getElementById('printers-grid');
grid.innerHTML = `
<div class="col-span-full text-center py-6 sm:py-12">
<svg class="h-12 w-12 sm:h-16 sm:w-16 text-red-500 dark:text-red-400 mx-auto mb-3 sm:mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-slate-700 dark:text-slate-300 text-base sm:text-lg">${message}</p>
<button onclick="refreshPrinters()" class="mt-3 sm:mt-4 bg-indigo-600 hover:bg-indigo-700 dark:bg-indigo-600 dark:hover:bg-indigo-500 text-white px-4 sm:px-6 py-1.5 sm:py-2 rounded-lg transition-all duration-200 text-sm sm:text-base">
Erneut versuchen
</button>
</div>
`;
}
// Add printer
async function handleAddPrinter(event) {
event.preventDefault();
const formData = new FormData(event.target);
const form = document.getElementById('addPrinterForm');
const formData = new FormData(form);
const printerData = {
name: formData.get('name'),
model: formData.get('model'),
location: formData.get('location'),
mac_address: formData.get('mac_address'),
plug_ip: formData.get('plug_ip')
plug_ip: formData.get('plug_ip'),
plug_username: formData.get('plug_username'),
plug_password: formData.get('plug_password')
};
try {
await apiCall('/api/printers', {
const response = await fetch('/api/printers/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
},
body: JSON.stringify(printerData)
});
showFlashMessage('Drucker erfolgreich hinzugefügt', 'success');
if (!response.ok) {
throw new Error('Fehler beim Hinzufügen des Druckers');
}
hideAddPrinterModal();
loadPrinters();
} catch (error) {
showFlashMessage('Fehler beim Hinzufügen des Druckers', 'error');
console.error('Error adding printer:', error);
alert('Fehler beim Hinzufügen des Druckers');
}
}
@@ -337,26 +427,91 @@
}
try {
await apiCall(`/api/printers/${printerId}`, {
method: 'DELETE'
const response = await fetch(`/api/printers/${printerId}`, {
method: 'DELETE',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
}
});
showFlashMessage('Drucker erfolgreich gelöscht', 'success');
if (!response.ok) {
throw new Error('Fehler beim Löschen des Druckers');
}
hidePrinterDetailModal();
loadPrinters();
} catch (error) {
showFlashMessage('Fehler beim Löschen des Druckers', 'error');
console.error('Error deleting printer:', error);
alert('Fehler beim Löschen des Druckers');
}
}
// Refresh printers
function refreshPrinters() {
showFlashMessage('Drucker werden aktualisiert...', 'info');
const grid = document.getElementById('printers-grid');
grid.innerHTML = `
<div class="col-span-full text-center py-6 sm:py-12">
<div class="animate-spin rounded-full h-8 w-8 sm:h-12 sm:w-12 border-b-2 border-indigo-600 dark:border-indigo-400 mx-auto"></div>
<p class="mt-3 sm:mt-4 text-sm sm:text-base text-slate-600 dark:text-slate-400">Lade Drucker...</p>
</div>
`;
loadPrinters();
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Lade Drucker beim Start
loadPrinters();
// Event-Listener für den "Drucker hinzufügen" Button
const addPrinterBtn = document.getElementById('addPrinterBtn');
if (addPrinterBtn) {
addPrinterBtn.addEventListener('click', showAddPrinterModal);
}
// Event-Listener für das Schließen des Modals
const closeAddPrinterBtn = document.getElementById('closeAddPrinterBtn');
if (closeAddPrinterBtn) {
closeAddPrinterBtn.addEventListener('click', hideAddPrinterModal);
}
const cancelAddPrinterBtn = document.getElementById('cancelAddPrinterBtn');
if (cancelAddPrinterBtn) {
cancelAddPrinterBtn.addEventListener('click', hideAddPrinterModal);
}
// Event-Listener für das Schließen des Detail-Modals
const closePrinterDetailBtn = document.getElementById('closePrinterDetailBtn');
if (closePrinterDetailBtn) {
closePrinterDetailBtn.addEventListener('click', hidePrinterDetailModal);
}
// Event-Listener für das Formular
const addPrinterForm = document.getElementById('addPrinterForm');
if (addPrinterForm) {
addPrinterForm.addEventListener('submit', handleAddPrinter);
}
// Modals schließen, wenn außerhalb geklickt wird
document.getElementById('addPrinterModal').addEventListener('click', function(e) {
if (e.target === this) {
hideAddPrinterModal();
}
});
document.getElementById('printerDetailModal').addEventListener('click', function(e) {
if (e.target === this) {
hidePrinterDetailModal();
}
});
// Modals schließen mit Escape-Taste
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
hideAddPrinterModal();
hidePrinterDetailModal();
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,116 @@
{% extends "base.html" %}
{% block title %}Datenschutzerklärung - MYP Platform{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Datenschutzerklärung</h1>
<p class="mt-2 text-slate-600 dark:text-slate-400 transition-colors duration-300">Gültig ab 15. Juni 2024</p>
</div>
<div class="glass-card mb-8">
<div class="prose dark:prose-invert max-w-none">
<h2>1. Verantwortliche Stelle</h2>
<p>Verantwortlich für die Datenverarbeitung im Rahmen der MYP-Plattform ist:</p>
<p>
Mercedes-Benz Group AG<br>
Mercedesstraße 120<br>
70372 Stuttgart<br>
Deutschland
</p>
<h2>2. Art der erhobenen Daten</h2>
<p>Im Rahmen der Nutzung der MYP-Plattform werden folgende personenbezogene Daten erhoben und verarbeitet:</p>
<ul>
<li>Personenstammdaten (Name, E-Mail-Adresse, Abteilung)</li>
<li>Benutzerkonto-Informationen (Benutzername, Passwort in verschlüsselter Form)</li>
<li>Nutzungsdaten (Druckaufträge, Zeitstempel, verwendete Geräte)</li>
<li>Protokolldaten (IP-Adresse, Zugriffszeiten, Aktivitäten)</li>
<li>Einstellungen und Präferenzen zur Personalisierung der Plattform</li>
</ul>
<h2>3. Zweck der Datenverarbeitung</h2>
<p>Die Verarbeitung der Daten erfolgt zu folgenden Zwecken:</p>
<ul>
<li>Bereitstellung und Verwaltung der MYP-Plattform</li>
<li>Authentifizierung und Autorisierung der Nutzer</li>
<li>Verwaltung und Optimierung von Druckaufträgen</li>
<li>Ressourcenplanung und -optimierung</li>
<li>Gewährleistung der Systemsicherheit</li>
<li>Erstellung anonymisierter Statistiken zur Systemnutzung</li>
</ul>
<h2>4. Rechtsgrundlage der Verarbeitung</h2>
<p>Die Datenverarbeitung erfolgt auf Grundlage:</p>
<ul>
<li>Der Erfüllung des Arbeitsverhältnisses gemäß Art. 6 Abs. 1 lit. b DSGVO</li>
<li>Der Wahrung berechtigter Interessen gemäß Art. 6 Abs. 1 lit. f DSGVO (effiziente Ressourcenverwaltung, Systemsicherheit)</li>
<li>Gegebenenfalls einer Einwilligung gemäß Art. 6 Abs. 1 lit. a DSGVO für optionale Funktionen</li>
</ul>
<h2>5. Speicherdauer</h2>
<p>Die personenbezogenen Daten werden nur so lange gespeichert, wie es für die genannten Zwecke erforderlich ist oder gesetzliche Aufbewahrungspflichten bestehen:</p>
<ul>
<li>Benutzerkontodaten: Für die Dauer der Unternehmenszugehörigkeit</li>
<li>Druckauftragsdaten: 12 Monate nach Abschluss des Auftrags</li>
<li>Protokolldaten: 90 Tage</li>
</ul>
<h2>6. Empfänger der Daten</h2>
<p>Die Daten werden ausschließlich innerhalb der Mercedes-Benz Group AG verarbeitet und nicht an externe Dritte weitergegeben, sofern keine gesetzliche Verpflichtung besteht.</p>
<p>Administratoren und IT-Mitarbeiter haben im Rahmen ihrer Aufgaben Zugriff auf die Daten, soweit dies für die Systemverwaltung erforderlich ist.</p>
<h2>7. Datensicherheit</h2>
<p>Mercedes-Benz trifft angemessene technische und organisatorische Maßnahmen, um die Sicherheit der personenbezogenen Daten zu gewährleisten. Dazu gehören:</p>
<ul>
<li>Verschlüsselung der Datenübertragung und -speicherung</li>
<li>Zugriffskontrollen und -beschränkungen</li>
<li>Regelmäßige Sicherheitsüberprüfungen und -aktualisierungen</li>
<li>Schulung der Mitarbeiter im Umgang mit personenbezogenen Daten</li>
</ul>
<h2>8. Rechte der betroffenen Personen</h2>
<p>Als Nutzer der MYP-Plattform haben Sie folgende Rechte:</p>
<ul>
<li>Recht auf Auskunft über die gespeicherten Daten (Art. 15 DSGVO)</li>
<li>Recht auf Berichtigung unrichtiger Daten (Art. 16 DSGVO)</li>
<li>Recht auf Löschung der Daten (Art. 17 DSGVO)</li>
<li>Recht auf Einschränkung der Verarbeitung (Art. 18 DSGVO)</li>
<li>Recht auf Datenübertragbarkeit (Art. 20 DSGVO)</li>
<li>Widerspruchsrecht gegen die Verarbeitung (Art. 21 DSGVO)</li>
</ul>
<h2>9. Datenschutzbeauftragter</h2>
<p>Bei Fragen zum Datenschutz oder zur Ausübung Ihrer Rechte können Sie sich an den Datenschutzbeauftragten der Mercedes-Benz Group AG wenden:</p>
<p>
Datenschutzbeauftragter<br>
Mercedes-Benz Group AG<br>
HPC G353<br>
70546 Stuttgart<br>
E-Mail: data.protection@mercedes-benz.com
</p>
<h2>10. Änderungen der Datenschutzerklärung</h2>
<p>Diese Datenschutzerklärung kann bei Bedarf aktualisiert werden. Die aktuelle Version ist jederzeit über die MYP-Plattform abrufbar.</p>
<h2>11. Beschwerderecht</h2>
<p>Sie haben das Recht, Beschwerde bei einer Datenschutz-Aufsichtsbehörde einzulegen, wenn Sie der Ansicht sind, dass die Verarbeitung Ihrer personenbezogenen Daten rechtswidrig erfolgt.</p>
<h2>12. Kontakt für Datenschutzfragen</h2>
<p>Bei Fragen zur Datenverarbeitung in der MYP-Plattform wenden Sie sich bitte an:</p>
<p>
<a href="mailto:till.tomczak@mercedes-benz.com" class="text-blue-600 dark:text-blue-400 hover:underline">till.tomczak@mercedes-benz.com</a><br>
<a href="mailto:torben.haack@mercedes-benz.com" class="text-blue-600 dark:text-blue-400 hover:underline">torben.haack@mercedes-benz.com</a>
</p>
</div>
</div>
<div class="flex justify-end">
<a href="javascript:history.back()" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-sm hover:shadow-md transition-all duration-300">
Zurück
</a>
</div>
</div>
{% endblock %}

View File

@@ -6,18 +6,18 @@
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-mercedes-black">Mein Profil</h1>
<p class="mt-2 text-mercedes-gray">Verwalten Sie Ihre Kontoinformationen und Einstellungen</p>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Mein Profil</h1>
<p class="mt-2 text-slate-500 dark:text-slate-400">Verwalten Sie Ihre Kontoinformationen und Einstellungen</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Profile Information -->
<div class="lg:col-span-2">
<div class="mercedes-card rounded-xl p-6 mb-6">
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50 mb-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">Persönliche Informationen</h2>
<h2 class="text-xl font-bold text-slate-900 dark:text-white">Persönliche Informationen</h2>
<button onclick="toggleEditMode()" id="edit-button"
class="bg-mercedes-blue hover:bg-blue-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-4 py-2 rounded-lg transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
@@ -28,61 +28,61 @@
<form id="profile-form" onsubmit="handleProfileUpdate(event)">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="first-name" class="block text-sm font-medium text-mercedes-black mb-2">
<label for="first-name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Vorname
</label>
<input type="text" id="first-name" name="first_name" value="{{ current_user.first_name or '' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="last-name" class="block text-sm font-medium text-mercedes-black mb-2">
<label for="last-name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Nachname
</label>
<input type="text" id="last-name" name="last_name" value="{{ current_user.last_name or '' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="email" class="block text-sm font-medium text-mercedes-black mb-2">
<label for="email" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
E-Mail-Adresse
</label>
<input type="email" id="email" name="email" value="{{ current_user.email }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="phone" class="block text-sm font-medium text-mercedes-black mb-2">
<label for="phone" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Telefonnummer
</label>
<input type="tel" id="phone" name="phone" value="{{ current_user.phone or '' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="department" class="block text-sm font-medium text-mercedes-black mb-2">
<label for="department" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Abteilung
</label>
<input type="text" id="department" name="department" value="{{ current_user.department or '' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="role" class="block text-sm font-medium text-mercedes-black mb-2">
<label for="role" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Rolle
</label>
<input type="text" id="role" value="{{ 'Administrator' if current_user.is_admin else 'Benutzer' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg bg-mercedes-light cursor-not-allowed">
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-gray-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 cursor-not-allowed">
</div>
</div>
<div id="form-actions" class="hidden mt-6 flex space-x-4">
<button type="submit"
class="bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
class="bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500 text-white px-6 py-2 rounded-lg transition-all duration-200">
Änderungen speichern
</button>
<button type="button" onclick="cancelEdit()"
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
class="bg-gray-200 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-white px-6 py-2 rounded-lg transition-all duration-200">
Abbrechen
</button>
</div>
@@ -90,37 +90,37 @@
</div>
<!-- Password Change -->
<div class="mercedes-card rounded-xl p-6">
<h2 class="text-xl font-bold text-mercedes-black mb-6">Passwort ändern</h2>
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-6">Passwort ändern</h2>
<form id="password-form" onsubmit="handlePasswordChange(event)" class="space-y-4">
<div>
<label for="current-password" class="block text-sm font-medium text-mercedes-black mb-2">
<label for="current-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Aktuelles Passwort
</label>
<input type="password" id="current-password" name="current_password" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
</div>
<div>
<label for="new-password" class="block text-sm font-medium text-mercedes-black mb-2">
<label for="new-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Neues Passwort
</label>
<input type="password" id="new-password" name="new_password" required minlength="8"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<p class="mt-1 text-xs text-mercedes-gray">Mindestens 8 Zeichen</p>
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">Mindestens 8 Zeichen</p>
</div>
<div>
<label for="confirm-password" class="block text-sm font-medium text-mercedes-black mb-2">
<label for="confirm-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Passwort bestätigen
</label>
<input type="password" id="confirm-password" name="confirm_password" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
</div>
<button type="submit"
class="bg-mercedes-blue hover:bg-blue-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-6 py-2 rounded-lg transition-all duration-200">
Passwort ändern
</button>
</form>
@@ -130,51 +130,51 @@
<!-- Sidebar -->
<div class="space-y-6">
<!-- Profile Stats -->
<div class="mercedes-card rounded-xl p-6">
<h3 class="text-lg font-bold text-mercedes-black mb-4">Statistiken</h3>
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Statistiken</h3>
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-sm text-mercedes-gray">Gesamte Aufträge</span>
<span id="total-jobs" class="text-lg font-bold text-mercedes-black">-</span>
<span class="text-sm text-slate-600 dark:text-slate-400">Gesamte Aufträge</span>
<span id="total-jobs" class="text-lg font-bold text-slate-900 dark:text-white">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-mercedes-gray">Abgeschlossene Aufträge</span>
<span id="completed-jobs" class="text-lg font-bold text-mercedes-green">-</span>
<span class="text-sm text-slate-600 dark:text-slate-400">Abgeschlossene Aufträge</span>
<span id="completed-jobs" class="text-lg font-bold text-green-600 dark:text-green-400">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-mercedes-gray">Aktive Aufträge</span>
<span id="active-jobs" class="text-lg font-bold text-mercedes-blue">-</span>
<span class="text-sm text-slate-600 dark:text-slate-400">Aktive Aufträge</span>
<span id="active-jobs" class="text-lg font-bold text-blue-600 dark:text-blue-400">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-mercedes-gray">Fehlgeschlagene Aufträge</span>
<span id="failed-jobs" class="text-lg font-bold text-mercedes-red">-</span>
<span class="text-sm text-slate-600 dark:text-slate-400">Fehlgeschlagene Aufträge</span>
<span id="failed-jobs" class="text-lg font-bold text-red-600 dark:text-red-400">-</span>
</div>
</div>
</div>
<!-- Account Info -->
<div class="mercedes-card rounded-xl p-6">
<h3 class="text-lg font-bold text-mercedes-black mb-4">Kontoinformationen</h3>
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Kontoinformationen</h3>
<div class="space-y-3 text-sm">
<div>
<span class="text-mercedes-gray">Mitglied seit:</span>
<div class="font-medium text-mercedes-black">{{ current_user.created_at.strftime('%d.%m.%Y') if current_user.created_at else 'Unbekannt' }}</div>
<span class="text-slate-600 dark:text-slate-400">Mitglied seit:</span>
<div class="font-medium text-slate-900 dark:text-white">{{ current_user.created_at.strftime('%d.%m.%Y') if current_user.created_at else 'Unbekannt' }}</div>
</div>
<div>
<span class="text-mercedes-gray">Letzte Anmeldung:</span>
<div class="font-medium text-mercedes-black">{{ current_user.last_login.strftime('%d.%m.%Y %H:%M') if current_user.last_login else 'Nie' }}</div>
<span class="text-slate-600 dark:text-slate-400">Letzte Anmeldung:</span>
<div class="font-medium text-slate-900 dark:text-white">{{ current_user.last_login.strftime('%d.%m.%Y %H:%M') if current_user.last_login else 'Nie' }}</div>
</div>
<div>
<span class="text-mercedes-gray">Konto-Status:</span>
<span class="text-slate-600 dark:text-slate-400">Konto-Status:</span>
<div class="font-medium">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-mercedes-green text-white">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-600 dark:bg-green-700 text-white">
Aktiv
</span>
</div>
@@ -183,22 +183,22 @@
</div>
<!-- Quick Actions -->
<div class="mercedes-card rounded-xl p-6">
<h3 class="text-lg font-bold text-mercedes-black mb-4">Schnellaktionen</h3>
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Schnellaktionen</h3>
<div class="space-y-3">
<a href="/new-job"
class="block w-full bg-mercedes-green hover:bg-green-700 text-white text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
class="block w-full bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500 text-white text-center py-2 px-4 rounded-lg transition-all duration-200">
Neuer Auftrag
</a>
<a href="/my/jobs"
class="block w-full bg-mercedes-blue hover:bg-blue-700 text-white text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
class="block w-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white text-center py-2 px-4 rounded-lg transition-all duration-200">
Meine Aufträge
</a>
<button onclick="downloadUserData()"
class="block w-full bg-mercedes-silver hover:bg-gray-400 text-mercedes-black text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
class="block w-full bg-gray-200 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-white text-center py-2 px-4 rounded-lg transition-all duration-200">
Daten exportieren
</button>
</div>

View File

@@ -0,0 +1,507 @@
{% extends "base.html" %}
{% block title %}Einstellungen - MYP Platform{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Einstellungen</h1>
<p class="mt-2 text-slate-600 dark:text-slate-400 transition-colors duration-300">Passen Sie die Anwendung an Ihre Bedürfnisse an</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Settings Sections (Left Side) -->
<div class="lg:col-span-2 space-y-8">
<!-- Appearance Settings -->
<div class="glass-card">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Erscheinungsbild</h2>
</div>
<div class="space-y-6">
<!-- Theme Settings -->
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Farbschema
</label>
<div class="flex items-center space-x-4">
<button id="light-theme-btn" class="theme-btn active px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
Hell
</button>
<button id="dark-theme-btn" class="theme-btn px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
Dunkel
</button>
<button id="system-theme-btn" class="theme-btn px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
System
</button>
</div>
</div>
<!-- Reduced Motion Settings -->
<div>
<div class="flex items-center justify-between">
<label for="reduced-motion" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Reduzierte Bewegung
</label>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="reduced-motion" name="reduced_motion" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="reduced-motion" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
Reduziert Animationen für bessere Barrierefreiheit
</p>
</div>
<!-- Contrast Settings -->
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Kontrast
</label>
<div class="flex items-center space-x-4">
<button id="normal-contrast-btn" class="contrast-btn active px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
Normal
</button>
<button id="high-contrast-btn" class="contrast-btn px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
Hoher Kontrast
</button>
</div>
</div>
</div>
</div>
<!-- Notification Settings -->
<div class="glass-card">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Benachrichtigungen</h2>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Neue Aufträge</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Benachrichtigung, wenn neue Aufträge erstellt werden</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="notify-new-jobs" name="notify_new_jobs" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
<label for="notify-new-jobs" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Auftragsaktualisierungen</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Benachrichtigung bei Statusänderungen Ihrer Aufträge</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="notify-job-updates" name="notify_job_updates" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
<label for="notify-job-updates" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Systembenachrichtigungen</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Wichtige Systemhinweise und Wartungsmeldungen</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="notify-system" name="notify_system" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
<label for="notify-system" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">E-Mail-Benachrichtigungen</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Zusammenfassung per E-Mail erhalten</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="notify-email" name="notify_email" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="notify-email" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
</div>
</div>
<!-- Privacy & Security -->
<div class="glass-card">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Datenschutz & Sicherheit</h2>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Aktivitätsprotokolle</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Protokollieren Sie Ihre Aktivitäten für erhöhte Sicherheit</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="activity-logs" name="activity_logs" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
<label for="activity-logs" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Zwei-Faktor-Authentifizierung</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Zusätzliche Sicherheitsebene für Ihr Konto</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="two-factor" name="two_factor" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="two-factor" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Automatische Abmeldung</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Nach einer bestimmten Zeit der Inaktivität abmelden</p>
</div>
<select id="auto-logout" name="auto_logout" class="form-select rounded-lg bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 text-slate-900 dark:text-white py-1 px-3 text-sm">
<option value="never">Nie</option>
<option value="30">Nach 30 Minuten</option>
<option value="60" selected>Nach 1 Stunde</option>
<option value="120">Nach 2 Stunden</option>
<option value="480">Nach 8 Stunden</option>
</select>
</div>
</div>
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
<button id="save-security-settings" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-sm hover:shadow-md transition-all duration-300">
Sicherheitseinstellungen speichern
</button>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Settings Navigation -->
<div class="glass-card">
<h3 class="text-lg font-bold text-slate-900 dark:text-white transition-colors duration-300 mb-4">Einstellungen</h3>
<nav class="space-y-1">
<a href="#appearance" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
Erscheinungsbild
</a>
<a href="#notifications" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg text-slate-900 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors duration-200">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
Benachrichtigungen
</a>
<a href="#privacy" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg text-slate-900 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors duration-200">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Datenschutz & Sicherheit
</a>
<a href="/user/profile" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg text-slate-900 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors duration-200">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Profil
</a>
</nav>
</div>
<!-- About System -->
<div class="glass-card">
<h3 class="text-lg font-bold text-slate-900 dark:text-white transition-colors duration-300 mb-4">Über das System</h3>
<div class="space-y-3 text-sm">
<div>
<span class="text-slate-600 dark:text-slate-400">Version:</span>
<div class="font-medium text-slate-900 dark:text-white">3.0.0</div>
</div>
<div>
<span class="text-slate-600 dark:text-slate-400">Letzte Aktualisierung:</span>
<div class="font-medium text-slate-900 dark:text-white">15.06.2024</div>
</div>
<div>
<span class="text-slate-600 dark:text-slate-400">Support:</span>
<div class="font-medium text-blue-600 dark:text-blue-400">
<a href="mailto:till.tomczak@mercedes-benz.com" class="hover:underline">till.tomczak@mercedes-benz.com</a>
<br>
<a href="mailto:torben.haack@mercedes-benz.com" class="hover:underline">torben.haack@mercedes-benz.com</a>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-slate-700">
<a href="/terms" class="text-blue-600 dark:text-blue-400 hover:underline text-sm">
Nutzungsbedingungen
</a>
<span class="text-slate-400 mx-2">|</span>
<a href="/privacy" class="text-blue-600 dark:text-blue-400 hover:underline text-sm">
Datenschutzerklärung
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Theme Switcher
const lightThemeBtn = document.getElementById('light-theme-btn');
const darkThemeBtn = document.getElementById('dark-theme-btn');
const systemThemeBtn = document.getElementById('system-theme-btn');
const themeBtns = [lightThemeBtn, darkThemeBtn, systemThemeBtn];
// Initialize button states based on current theme
const STORAGE_KEY = 'myp-dark-mode';
const savedMode = localStorage.getItem(STORAGE_KEY);
if (savedMode === 'true') {
setActiveThemeButton(darkThemeBtn);
} else if (savedMode === 'false') {
setActiveThemeButton(lightThemeBtn);
} else {
setActiveThemeButton(systemThemeBtn);
}
// Theme button click handlers
lightThemeBtn.addEventListener('click', function() {
document.documentElement.classList.remove('dark');
localStorage.setItem(STORAGE_KEY, 'false');
setActiveThemeButton(lightThemeBtn);
});
darkThemeBtn.addEventListener('click', function() {
document.documentElement.classList.add('dark');
localStorage.setItem(STORAGE_KEY, 'true');
setActiveThemeButton(darkThemeBtn);
});
systemThemeBtn.addEventListener('click', function() {
localStorage.removeItem(STORAGE_KEY);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
setActiveThemeButton(systemThemeBtn);
});
function setActiveThemeButton(activeBtn) {
themeBtns.forEach(btn => {
if (btn === activeBtn) {
btn.classList.add('active');
btn.classList.add('bg-blue-50');
btn.classList.add('dark:bg-blue-900/20');
btn.classList.add('text-blue-700');
btn.classList.add('dark:text-blue-300');
btn.classList.remove('bg-white');
btn.classList.remove('dark:bg-slate-800');
btn.classList.remove('text-slate-900');
btn.classList.remove('dark:text-white');
} else {
btn.classList.remove('active');
btn.classList.remove('bg-blue-50');
btn.classList.remove('dark:bg-blue-900/20');
btn.classList.remove('text-blue-700');
btn.classList.remove('dark:text-blue-300');
btn.classList.add('bg-white');
btn.classList.add('dark:bg-slate-800');
btn.classList.add('text-slate-900');
btn.classList.add('dark:text-white');
}
});
}
// Contrast Settings
const normalContrastBtn = document.getElementById('normal-contrast-btn');
const highContrastBtn = document.getElementById('high-contrast-btn');
const contrastBtns = [normalContrastBtn, highContrastBtn];
normalContrastBtn.addEventListener('click', function() {
document.documentElement.classList.remove('high-contrast');
localStorage.setItem('myp-contrast', 'normal');
setActiveContrastButton(normalContrastBtn);
});
highContrastBtn.addEventListener('click', function() {
document.documentElement.classList.add('high-contrast');
localStorage.setItem('myp-contrast', 'high');
setActiveContrastButton(highContrastBtn);
});
function setActiveContrastButton(activeBtn) {
contrastBtns.forEach(btn => {
if (btn === activeBtn) {
btn.classList.add('active');
btn.classList.add('bg-blue-50');
btn.classList.add('dark:bg-blue-900/20');
btn.classList.add('text-blue-700');
btn.classList.add('dark:text-blue-300');
btn.classList.remove('bg-white');
btn.classList.remove('dark:bg-slate-800');
btn.classList.remove('text-slate-900');
btn.classList.remove('dark:text-white');
} else {
btn.classList.remove('active');
btn.classList.remove('bg-blue-50');
btn.classList.remove('dark:bg-blue-900/20');
btn.classList.remove('text-blue-700');
btn.classList.remove('dark:text-blue-300');
btn.classList.add('bg-white');
btn.classList.add('dark:bg-slate-800');
btn.classList.add('text-slate-900');
btn.classList.add('dark:text-white');
}
});
}
// Save Settings Button
const saveSecurityBtn = document.getElementById('save-security-settings');
saveSecurityBtn.addEventListener('click', function() {
saveAllSettings();
});
// Toggle Switch Styling
document.querySelectorAll('.toggle-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', function() {
// Auto-save bei Änderung
const settingName = this.name.replace('_', ' ');
const status = this.checked ? 'aktiviert' : 'deaktiviert';
showFlashMessage(`${settingName} wurde ${status}`, 'info');
});
});
// Sammle alle Einstellungen und speichere sie
async function saveAllSettings() {
try {
// Erscheinungsbild-Einstellungen
const theme = localStorage.getItem(STORAGE_KEY) === 'true' ? 'dark' :
localStorage.getItem(STORAGE_KEY) === 'false' ? 'light' : 'system';
const reducedMotion = document.getElementById('reduced-motion').checked;
const contrast = localStorage.getItem('myp-contrast') || 'normal';
// Benachrichtigungseinstellungen
const notifyNewJobs = document.getElementById('notify-new-jobs').checked;
const notifyJobUpdates = document.getElementById('notify-job-updates').checked;
const notifySystem = document.getElementById('notify-system').checked;
const notifyEmail = document.getElementById('notify-email').checked;
// Datenschutz & Sicherheitseinstellungen
const activityLogs = document.getElementById('activity-logs').checked;
const twoFactor = document.getElementById('two-factor').checked;
const autoLogout = document.getElementById('auto-logout').value;
// Einstellungsobjekt erstellen
const settings = {
theme: theme,
reduced_motion: reducedMotion,
contrast: contrast,
notifications: {
new_jobs: notifyNewJobs,
job_updates: notifyJobUpdates,
system: notifySystem,
email: notifyEmail
},
privacy: {
activity_logs: activityLogs,
two_factor: twoFactor,
auto_logout: autoLogout
}
};
// Einstellungen an den Server senden
const response = await apiCall('/user/update-settings', {
method: 'POST',
body: JSON.stringify(settings)
});
if (response.success) {
showFlashMessage('Alle Einstellungen wurden erfolgreich gespeichert', 'success');
} else {
throw new Error(response.message || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Fehler beim Speichern der Einstellungen:', error);
showFlashMessage('Fehler beim Speichern der Einstellungen: ' + error.message, 'error');
}
}
// Helper function to show flash messages
function showFlashMessage(message, type = 'info') {
// Use the global toast manager if available
if (window.showToast) {
window.showToast(message, type);
} else {
alert(message);
}
}
// Navigation links
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', function(e) {
// If it's a real link to another page, don't preventDefault
if (this.getAttribute('href').startsWith('#')) {
e.preventDefault();
// Update active state
navItems.forEach(navItem => {
navItem.classList.remove('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-700', 'dark:text-blue-300');
navItem.classList.add('text-slate-900', 'dark:text-white', 'hover:bg-gray-100', 'dark:hover:bg-slate-800');
});
this.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-700', 'dark:text-blue-300');
this.classList.remove('text-slate-900', 'dark:text-white', 'hover:bg-gray-100', 'dark:hover:bg-slate-800');
// Scroll to section
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.querySelector(`[id="${targetId}"]`) ||
document.querySelector(`h2:contains("${targetId}")`);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth' });
}
}
});
});
});
</script>
<style>
/* Toggle Switch Styling */
.toggle-checkbox:checked {
right: 0;
border-color: #60a5fa;
@apply bg-blue-500;
}
.toggle-checkbox:checked + .toggle-label {
@apply bg-blue-500 dark:bg-blue-600;
}
.toggle-label {
transition: background-color 0.3s ease;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,464 @@
{% extends "base.html" %}
{% block title %}Statistiken - MYP Platform{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Header -->
<div class="relative overflow-hidden rounded-2xl p-6 stats-card">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Statistiken</h1>
<p class="mt-2 text-slate-500 dark:text-slate-400">Übersicht über Systemleistung und Nutzungsstatistiken</p>
</div>
<div class="flex gap-4 mt-4 lg:mt-0">
<button onclick="refreshStats()" class="bg-black hover:bg-gray-800 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-6 py-2.5 rounded-xl transition-colors duration-300">
<svg class="h-5 w-5 mr-2 inline" 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>
Aktualisieren
</button>
<button onclick="exportStats()" class="bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-700 dark:text-white px-6 py-2.5 rounded-xl transition-colors duration-300 border border-slate-200 dark:border-slate-600">
<svg class="h-5 w-5 mr-2 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
Exportieren
</button>
</div>
</div>
</div>
<!-- Overview Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<!-- Total Jobs -->
<div class="stats-card p-6" data-api-endpoint="/api/stats/total-jobs" data-counter>
<div class="absolute top-5 right-5 text-slate-900 dark:text-blue-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 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 2" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Gesamte Jobs</p>
{% if total_jobs is defined and total_jobs %}
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ total_jobs }}</p>
{% endif %}
</div>
<!-- Completed Jobs -->
<div class="stats-card p-6" data-api-endpoint="/api/stats/completed-jobs" data-counter>
<div class="absolute top-5 right-5 text-green-600 dark:text-green-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Abgeschlossene Jobs</p>
{% if completed_jobs is defined and completed_jobs %}
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ completed_jobs }}</p>
{% endif %}
</div>
<!-- Active Printers -->
<div class="stats-card p-6" data-api-endpoint="/api/stats/active-printers" data-counter>
<div class="absolute top-5 right-5 text-purple-600 dark:text-purple-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Aktive Drucker</p>
{% if active_printers is defined and active_printers %}
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ active_printers }}</p>
{% endif %}
</div>
<!-- Total Print Time -->
<div class="stats-card p-6" data-api-endpoint="/api/stats/print-time" data-counter>
<div class="absolute top-5 right-5 text-amber-600 dark:text-amber-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Gesamte Druckzeit</p>
{% if total_print_time is defined and total_print_time %}
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ total_print_time }}</p>
{% endif %}
</div>
</div>
<!-- Charts and Detailed Stats -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Job Status Distribution -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Job-Status Verteilung</h2>
<div id="job-status-chart" class="h-64" data-api-endpoint="/api/stats/job-status" data-chart>
{% if job_status_data is defined and job_status_data and job_status_data|length > 0 %}
<!-- Recharts wird hier dynamisch gerendert -->
{% endif %}
</div>
</div>
<!-- Printer Usage -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Drucker-Nutzung</h2>
<div id="printer-usage-chart" class="h-64" data-api-endpoint="/api/stats/printer-usage" data-chart>
{% if printer_usage_data is defined and printer_usage_data and printer_usage_data|length > 0 %}
<!-- Recharts wird hier dynamisch gerendert -->
{% endif %}
</div>
</div>
</div>
<!-- Detailed Statistics Tables -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Recent Activity -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Letzte Aktivitäten</h2>
<div id="recent-activity" class="space-y-4" data-api-endpoint="/api/stats/activity" data-list>
{% if recent_activity is defined and recent_activity and recent_activity|length > 0 %}
{% for activity in recent_activity %}
<div class="border-l-4 border-black dark:border-blue-500 pl-4 py-3 bg-slate-50 dark:bg-slate-700/30 rounded-r-xl">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-slate-900 dark:text-white">{{ activity.description }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">{{ activity.timestamp|format_datetime }}</p>
</div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
<!-- Top Users -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Top Benutzer</h2>
<div id="top-users" class="space-y-4" data-api-endpoint="/api/stats/users" data-list>
{% if top_users is defined and top_users and top_users|length > 0 %}
{% for user in top_users %}
<div class="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
<div class="flex items-center">
<div class="mr-3 text-lg font-bold text-slate-900 dark:text-blue-400">#{{ loop.index }}</div>
<div>
<p class="text-sm font-medium text-slate-900 dark:text-white">{{ user.name }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">{{ user.email }}</p>
</div>
</div>
<div class="text-right">
<p class="text-lg font-bold text-slate-900 dark:text-white">{{ user.job_count }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">Jobs</p>
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
<!-- System Performance Metrics -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Systemleistung</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Average Job Duration -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl" data-api-endpoint="/api/stats/job-duration" data-counter>
<svg class="h-8 w-8 mx-auto mb-3 text-slate-900 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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-sm text-slate-500 dark:text-slate-400 mb-1">Durchschnittliche Job-Dauer</p>
{% if avg_job_duration is defined and avg_job_duration %}
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ avg_job_duration }}</p>
{% endif %}
</div>
<!-- Success Rate -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl" data-api-endpoint="/api/stats/success-rate" data-counter>
<svg class="h-8 w-8 mx-auto mb-3 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Erfolgsrate</p>
{% if success_rate is defined and success_rate %}
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ success_rate }}%</p>
{% endif %}
</div>
<!-- System Uptime -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl" data-api-endpoint="/api/stats/uptime" data-counter>
<svg class="h-8 w-8 mx-auto mb-3 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">System-Verfügbarkeit</p>
{% if system_uptime is defined and system_uptime %}
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ system_uptime }}%</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- ApexCharts Integration für unsere Diagramme -->
<script>
document.addEventListener('DOMContentLoaded', function() {
loadStats();
// Theme wechsel Event-Listener
window.addEventListener('darkModeChanged', function(e) {
// Charts neu rendern mit passendem Farbschema
if (window.jobStatusChart) {
updateChartTheme(window.jobStatusChart, e.detail.isDark);
}
if (window.printerUsageChart) {
updateChartTheme(window.printerUsageChart, e.detail.isDark);
}
});
function loadStats() {
const counterElements = document.querySelectorAll('[data-counter]');
const chartElements = document.querySelectorAll('[data-chart]');
const listElements = document.querySelectorAll('[data-list]');
// Zähler laden
counterElements.forEach(el => {
const endpoint = el.getAttribute('data-api-endpoint');
if (endpoint) {
fetch(endpoint)
.then(response => response.json())
.then(data => {
el.querySelector('p:last-child').textContent = data.value || '0';
})
.catch(error => console.error('Error loading counter:', error));
}
});
// Diagramme laden (vereinfachte Implementierung)
chartElements.forEach(el => {
const endpoint = el.getAttribute('data-api-endpoint');
if (endpoint) {
fetch(endpoint)
.then(response => response.json())
.then(data => {
const chartId = el.id;
if (chartId === 'job-status-chart') {
renderJobStatusChart(el, data);
} else if (chartId === 'printer-usage-chart') {
renderPrinterUsageChart(el, data);
}
})
.catch(error => {
console.error(`Fehler beim Laden der Diagrammdaten von ${endpoint}:`, error);
el.innerHTML = '<div class="flex items-center justify-center h-full"><p class="text-red-500">Fehler beim Laden der Daten</p></div>';
});
}
});
// Listen laden
listElements.forEach(el => {
const endpoint = el.getAttribute('data-api-endpoint');
if (endpoint) {
fetch(endpoint)
.then(response => response.json())
.then(data => {
const listId = el.id;
if (listId === 'recent-activity') {
renderActivityList(el, data.activities || []);
} else if (listId === 'top-users') {
renderUsersList(el, data.users || []);
}
})
.catch(error => {
console.error(`Fehler beim Laden der Listendaten von ${endpoint}:`, error);
el.innerHTML = '<div class="p-4"><p class="text-red-500">Fehler beim Laden der Daten</p></div>';
});
}
});
}
function renderJobStatusChart(element, data) {
const chartData = data.data || [];
const labels = data.labels || ['Abgeschlossen', 'In Bearbeitung', 'Fehler', 'Wartend', 'Abgebrochen'];
const series = chartData.length > 0 ? chartData : [44, 55, 13, 33, 22]; // Fallback-Daten
const isDark = document.documentElement.classList.contains('dark');
const options = {
series: series,
chart: {
type: 'donut',
height: 240,
fontFamily: 'Inter, sans-serif',
foreColor: isDark ? '#94a3b8' : '#64748b',
},
labels: labels,
colors: isDark ? ['#10b981', '#3b82f6', '#ef4444', '#f59e0b', '#6b7280'] : ['#10b981', '#000000', '#ef4444', '#f59e0b', '#6b7280'],
plotOptions: {
pie: {
donut: {
size: '60%'
},
expandOnClick: true
}
},
legend: {
position: 'bottom'
},
tooltip: {
theme: isDark ? 'dark' : 'light'
}
};
// Element leeren und Chart erstellen
element.innerHTML = '';
window.jobStatusChart = new ApexCharts(element, options);
window.jobStatusChart.render();
}
function renderPrinterUsageChart(element, data) {
const printerNames = data.categories || ['Drucker 1', 'Drucker 2', 'Drucker 3', 'Drucker 4', 'Drucker 5'];
const usageData = data.series || [30, 40, 25, 50, 49]; // Fallback-Daten
const isDark = document.documentElement.classList.contains('dark');
const options = {
series: [{
name: 'Druckzeit (Stunden)',
data: usageData
}],
chart: {
type: 'bar',
height: 240,
fontFamily: 'Inter, sans-serif',
toolbar: {
show: false
},
foreColor: isDark ? '#94a3b8' : '#64748b',
},
plotOptions: {
bar: {
borderRadius: 4,
horizontal: false,
columnWidth: '60%',
}
},
dataLabels: {
enabled: false
},
colors: [isDark ? '#3b82f6' : '#000000'],
xaxis: {
categories: printerNames,
labels: {
style: {
fontSize: '12px'
}
}
},
yaxis: {
title: {
text: 'Stunden'
}
},
tooltip: {
theme: isDark ? 'dark' : 'light'
}
};
// Element leeren und Chart erstellen
element.innerHTML = '';
window.printerUsageChart = new ApexCharts(element, options);
window.printerUsageChart.render();
}
function renderActivityList(element, activities) {
if (!activities || activities.length === 0) {
element.innerHTML = '<div class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl"><p class="text-center text-slate-500 dark:text-slate-400">Keine Aktivitäten gefunden</p></div>';
return;
}
let html = '';
activities.forEach(activity => {
const timestamp = activity.timestamp ? new Date(activity.timestamp).toLocaleString('de-DE') : '';
html += `
<div class="border-l-4 border-black dark:border-blue-500 pl-4 py-3 bg-slate-50 dark:bg-slate-700/30 rounded-r-xl">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-slate-900 dark:text-white">${activity.description}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">${timestamp}</p>
</div>
</div>
</div>
`;
});
element.innerHTML = html;
}
function renderUsersList(element, users) {
if (!users || users.length === 0) {
element.innerHTML = '<div class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl"><p class="text-center text-slate-500 dark:text-slate-400">Keine Benutzer gefunden</p></div>';
return;
}
let html = '';
users.forEach((user, index) => {
html += `
<div class="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
<div class="flex items-center">
<div class="mr-3 text-lg font-bold text-slate-900 dark:text-blue-400">#${index + 1}</div>
<div>
<p class="text-sm font-medium text-slate-900 dark:text-white">${user.name}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">${user.email}</p>
</div>
</div>
<div class="text-right">
<p class="text-lg font-bold text-slate-900 dark:text-white">${user.job_count || 0}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">Jobs</p>
</div>
</div>
`;
});
element.innerHTML = html;
}
function updateChartTheme(chart, isDark) {
chart.updateOptions({
chart: {
foreColor: isDark ? '#94a3b8' : '#64748b'
},
tooltip: {
theme: isDark ? 'dark' : 'light'
},
colors: isDark ? ['#10b981', '#3b82f6', '#ef4444', '#f59e0b', '#6b7280'] : ['#10b981', '#000000', '#ef4444', '#f59e0b', '#6b7280']
});
}
// Statistiken neu laden
function refreshStats() {
// Feedback für den Benutzer
showToast('Statistiken werden aktualisiert...', 'info');
// Daten neu laden
loadStats();
// Erfolgsmeldung
setTimeout(() => {
showToast('Statistiken erfolgreich aktualisiert', 'success');
}, 1000);
}
// Statistiken exportieren
function exportStats() {
// Direkter Download vom API-Endpunkt
window.location.href = '/api/stats/export';
}
// Helper-Funktion für Toast-Benachrichtigungen
function showToast(message, type) {
if (window.showToast) {
window.showToast(message, type);
} else {
alert(message);
}
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,82 @@
{% extends "base.html" %}
{% block title %}Nutzungsbedingungen - MYP Platform{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Nutzungsbedingungen</h1>
<p class="mt-2 text-slate-600 dark:text-slate-400 transition-colors duration-300">Gültig ab 15. Juni 2024</p>
</div>
<div class="glass-card mb-8">
<div class="prose dark:prose-invert max-w-none">
<h2>1. Allgemeines</h2>
<p>Diese Nutzungsbedingungen regeln die Nutzung der Mercedes-Benz "Manage Your Printers" (MYP) Plattform. Durch die Nutzung der Plattform erklären Sie sich mit diesen Bedingungen einverstanden.</p>
<h2>2. Zugang und Nutzungsberechtigung</h2>
<p>Die MYP-Plattform steht ausschließlich Mitarbeitern der Mercedes-Benz Group AG und ihren verbundenen Unternehmen zur Verfügung. Der Zugang erfolgt über eine persönliche Benutzerkennung und ein Passwort, die nicht an Dritte weitergegeben werden dürfen.</p>
<h2>3. Nutzungszweck</h2>
<p>Die MYP-Plattform dient ausschließlich der Verwaltung und Überwachung von 3D-Druckaufträgen im Rahmen der beruflichen Tätigkeit. Eine private Nutzung ist nicht gestattet.</p>
<h2>4. Verantwortlichkeiten der Nutzer</h2>
<p>Als Nutzer sind Sie verantwortlich für:</p>
<ul>
<li>Die Geheimhaltung Ihrer Zugangsdaten</li>
<li>Die ordnungsgemäße Nutzung der Geräte und Ressourcen</li>
<li>Die Einhaltung der Unternehmensrichtlinien zum Umgang mit 3D-Druckern</li>
<li>Die Beachtung von Urheberrechten und Schutzrechten Dritter bei der Erstellung von 3D-Modellen</li>
</ul>
<h2>5. Einschränkungen</h2>
<p>Es ist untersagt:</p>
<ul>
<li>Die Plattform für nicht-geschäftliche Zwecke zu nutzen</li>
<li>Unbefugten Zugang zu verschaffen</li>
<li>Die Sicherheitsmaßnahmen der Plattform zu umgehen</li>
<li>Schädlichen Code oder Malware hochzuladen</li>
<li>Die Plattform zu überlasten oder ihre normale Funktion zu stören</li>
</ul>
<h2>6. Verfügbarkeit und Wartung</h2>
<p>Mercedes-Benz bemüht sich um eine hohe Verfügbarkeit der MYP-Plattform, kann jedoch keine ununterbrochene Verfügbarkeit garantieren. Wartungsarbeiten werden nach Möglichkeit im Voraus angekündigt.</p>
<h2>7. Haftung</h2>
<p>Mercedes-Benz übernimmt keine Haftung für:</p>
<ul>
<li>Schäden, die durch fehlerhafte Druckaufträge entstehen</li>
<li>Verlust von Daten oder Modellen</li>
<li>Ausfallzeiten der Plattform</li>
<li>Schäden durch unsachgemäße Verwendung der Drucker</li>
</ul>
<h2>8. Datenschutz</h2>
<p>Die Erhebung und Verarbeitung personenbezogener Daten erfolgt gemäß der <a href="/privacy" class="text-blue-600 dark:text-blue-400 hover:underline">Datenschutzerklärung</a>. Die Daten werden ausschließlich zur Verwaltung und Optimierung der Druckaufträge verwendet.</p>
<h2>9. Änderungen der Nutzungsbedingungen</h2>
<p>Mercedes-Benz behält sich das Recht vor, diese Nutzungsbedingungen jederzeit zu ändern. Wesentliche Änderungen werden den Nutzern mitgeteilt.</p>
<h2>10. Beendigung der Nutzung</h2>
<p>Die Nutzungsberechtigung endet automatisch mit dem Ausscheiden aus dem Unternehmen. Mercedes-Benz behält sich das Recht vor, den Zugang bei Verstößen gegen diese Nutzungsbedingungen zu sperren.</p>
<h2>11. Anwendbares Recht</h2>
<p>Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.</p>
<h2>12. Kontakt</h2>
<p>Bei Fragen zu diesen Nutzungsbedingungen wenden Sie sich bitte an:</p>
<p>
<a href="mailto:till.tomczak@mercedes-benz.com" class="text-blue-600 dark:text-blue-400 hover:underline">till.tomczak@mercedes-benz.com</a><br>
<a href="mailto:torben.haack@mercedes-benz.com" class="text-blue-600 dark:text-blue-400 hover:underline">torben.haack@mercedes-benz.com</a>
</p>
</div>
</div>
<div class="flex justify-end">
<a href="javascript:history.back()" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-sm hover:shadow-md transition-all duration-300">
Zurück
</a>
</div>
</div>
{% endblock %}