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:
47
backend/app/templates/404.html
Normal file
47
backend/app/templates/404.html
Normal 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 %}
|
523
backend/app/templates/admin.html
Normal file
523
backend/app/templates/admin.html
Normal 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 %}
|
@@ -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>
|
||||
|
@@ -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 %}
|
46
backend/app/templates/index.html
Normal file
46
backend/app/templates/index.html
Normal 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
@@ -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 %}
|
@@ -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 %}
|
116
backend/app/templates/privacy.html
Normal file
116
backend/app/templates/privacy.html
Normal 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 %}
|
@@ -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>
|
||||
|
507
backend/app/templates/settings.html
Normal file
507
backend/app/templates/settings.html
Normal 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 %}
|
464
backend/app/templates/stats.html
Normal file
464
backend/app/templates/stats.html
Normal 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 %}
|
82
backend/app/templates/terms.html
Normal file
82
backend/app/templates/terms.html
Normal 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 %}
|
Reference in New Issue
Block a user