feat: Update frontend and backend configurations for development environment

- Downgrade PyP100 version in requirements.txt for compatibility.
- Add new frontend routes for index, login, dashboard, printers, jobs, and profile pages.
- Modify docker-compose files for development setup, including environment variables and service names.
- Update Caddyfile for local development with Raspberry Pi backend.
- Adjust health check route to use updated backend URL.
- Enhance setup-backend-url.sh for development environment configuration.
"""
This commit is contained in:
2025-05-24 18:58:17 +02:00
parent ead75ae451
commit 62e131c02f
19 changed files with 3433 additions and 105 deletions

View File

@@ -0,0 +1,504 @@
<!DOCTYPE html>
<html lang="de" class="h-full">
<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>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Custom CSS für Mercedes Farben -->
<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);
}
}
</style>
{% block head %}{% endblock %}
</head>
<body class="h-full bg-gradient-to-br from-mercedes-light-gray to-white font-sans">
<!-- 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>
</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
</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>
<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>
<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>
{% 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>
{% endif %}
</div>
</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
</button>
{% 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
</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>
</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>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,461 @@
{% extends "base.html" %}
{% block title %}Dashboard - MYP Platform{% 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>
<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">
<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>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Neuer Job
</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>
</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>
</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>
</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>
</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>
</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>
</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>
</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>
</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>
</div>
</div>
</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>
</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" />
</svg>
Admin-Panel
</a>
</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 %}
<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);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,469 @@
{% extends "base.html" %}
{% block title %}Druckaufträge - MYP Platform{% 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">Druckaufträge</h1>
<p class="mt-2 text-mercedes-gray">Verwalten Sie Ihre 3D-Druckaufträge</p>
</div>
<div class="flex space-x-4">
<button onclick="refreshJobs()" 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">
<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>
<a href="/new-job" 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Neuer Auftrag
</a>
</div>
</div>
</div>
<!-- Filter Bar -->
<div class="mb-6 mercedes-card rounded-xl p-4">
<div class="flex flex-wrap items-center gap-4">
<div class="flex items-center space-x-2">
<label for="status-filter" class="text-sm font-medium text-mercedes-black">Status:</label>
<select id="status-filter" onchange="filterJobs()" class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Alle</option>
<option value="pending">Wartend</option>
<option value="printing">Druckt</option>
<option value="completed">Abgeschlossen</option>
<option value="failed">Fehlgeschlagen</option>
<option value="cancelled">Abgebrochen</option>
</select>
</div>
<div class="flex items-center space-x-2">
<label for="printer-filter" class="text-sm font-medium text-mercedes-black">Drucker:</label>
<select id="printer-filter" onchange="filterJobs()" class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Alle Drucker</option>
</select>
</div>
<div class="flex items-center space-x-2">
<label for="search-input" class="text-sm font-medium text-mercedes-black">Suche:</label>
<input type="text" id="search-input" placeholder="Dateiname..." onkeyup="filterJobs()"
class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<button onclick="clearFilters()" class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-3 py-1 rounded-lg text-sm mercedes-button transition-all duration-200">
Filter zurücksetzen
</button>
</div>
</div>
<!-- Jobs Table -->
<div class="mercedes-card rounded-xl overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-mercedes-silver">
<thead class="bg-mercedes-light">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
Datei
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
Status
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
Drucker
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
Fortschritt
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
Erstellt
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
Aktionen
</th>
</tr>
</thead>
<tbody id="jobs-table-body" class="bg-white divide-y divide-mercedes-silver">
<!-- Loading state -->
<tr>
<td colspan="6" class="px-6 py-12 text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-mercedes-blue mx-auto"></div>
<p class="mt-4 text-mercedes-gray">Lade Aufträge...</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
<div id="pagination" class="mt-6 flex items-center justify-between">
<div class="text-sm text-mercedes-gray">
<span id="pagination-info">Zeige 0 von 0 Aufträgen</span>
</div>
<div class="flex space-x-2">
<button id="prev-page" onclick="changePage(-1)" disabled
class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-mercedes-light transition-all duration-200">
Zurück
</button>
<span id="page-numbers" class="flex space-x-1">
<!-- Page numbers will be inserted here -->
</span>
<button id="next-page" onclick="changePage(1)" disabled
class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-mercedes-light transition-all duration-200">
Weiter
</button>
</div>
</div>
</div>
<!-- Job Detail Modal -->
<div id="jobDetailModal" 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-2xl max-h-screen overflow-y-auto">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">Auftrag Details</h2>
<button onclick="hideJobDetailModal()" class="text-mercedes-gray hover:text-mercedes-black">
<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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div id="job-detail-content">
<!-- Content will be loaded here -->
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let jobs = [];
let filteredJobs = [];
let printers = [];
let currentPage = 1;
const jobsPerPage = 10;
// Load data
async function loadJobs() {
try {
const [jobsResponse, printersResponse] = await Promise.all([
apiCall('/api/jobs'),
apiCall('/api/printers')
]);
jobs = jobsResponse.jobs || [];
printers = printersResponse.printers || [];
populatePrinterFilter();
filterJobs();
} catch (error) {
console.error('Error loading jobs:', error);
showFlashMessage('Fehler beim Laden der Aufträge', 'error');
}
}
// Populate printer filter
function populatePrinterFilter() {
const select = document.getElementById('printer-filter');
select.innerHTML = '<option value="">Alle Drucker</option>';
printers.forEach(printer => {
const option = document.createElement('option');
option.value = printer.id;
option.textContent = printer.name;
select.appendChild(option);
});
}
// Filter jobs
function filterJobs() {
const statusFilter = document.getElementById('status-filter').value;
const printerFilter = document.getElementById('printer-filter').value;
const searchTerm = document.getElementById('search-input').value.toLowerCase();
filteredJobs = jobs.filter(job => {
const matchesStatus = !statusFilter || job.status === statusFilter;
const matchesPrinter = !printerFilter || job.printer_id == printerFilter;
const matchesSearch = !searchTerm || job.filename.toLowerCase().includes(searchTerm);
return matchesStatus && matchesPrinter && matchesSearch;
});
currentPage = 1;
renderJobs();
updatePagination();
}
// Clear filters
function clearFilters() {
document.getElementById('status-filter').value = '';
document.getElementById('printer-filter').value = '';
document.getElementById('search-input').value = '';
filterJobs();
}
// Render jobs table
function renderJobs() {
const tbody = document.getElementById('jobs-table-body');
if (filteredJobs.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="px-6 py-12 text-center">
<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 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>
<p class="text-mercedes-gray text-lg">Keine Aufträge gefunden</p>
<a href="/new-job" class="mt-4 inline-block bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
Ersten Auftrag erstellen
</a>
</td>
</tr>
`;
return;
}
const startIndex = (currentPage - 1) * jobsPerPage;
const endIndex = startIndex + jobsPerPage;
const pageJobs = filteredJobs.slice(startIndex, endIndex);
tbody.innerHTML = pageJobs.map(job => {
const printer = printers.find(p => p.id === job.printer_id);
const statusColor = getJobStatusColor(job.status);
const statusText = getJobStatusText(job.status);
return `
<tr class="hover:bg-mercedes-light transition-colors duration-200">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<svg class="h-8 w-8 text-mercedes-blue mr-3" 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>
<div>
<div class="text-sm font-medium text-mercedes-black">${job.filename}</div>
<div class="text-sm text-mercedes-gray">${formatFileSize(job.file_size)}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}">
${statusText}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-mercedes-black">
${printer ? printer.name : 'Unbekannt'}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="w-full bg-mercedes-silver rounded-full h-2">
<div class="bg-mercedes-blue h-2 rounded-full" style="width: ${job.progress || 0}%"></div>
</div>
<div class="text-xs text-mercedes-gray mt-1">${job.progress || 0}%</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-mercedes-gray">
${formatDate(job.created_at)}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2">
<button onclick="showJobDetail(${job.id})"
class="text-mercedes-blue hover:text-blue-700 transition-colors duration-200">
Details
</button>
${job.status === 'pending' ? `
<button onclick="cancelJob(${job.id})"
class="text-mercedes-red hover:text-red-700 transition-colors duration-200">
Abbrechen
</button>
` : ''}
</div>
</td>
</tr>
`;
}).join('');
}
// Helper functions
function getJobStatusColor(status) {
switch (status) {
case 'pending': return 'bg-mercedes-yellow text-mercedes-black';
case 'printing': return 'bg-mercedes-blue text-white';
case 'completed': return 'bg-mercedes-green text-white';
case 'failed': return 'bg-mercedes-red text-white';
case 'cancelled': return 'bg-mercedes-gray text-white';
default: return 'bg-mercedes-silver text-mercedes-black';
}
}
function getJobStatusText(status) {
switch (status) {
case 'pending': return 'Wartend';
case 'printing': return 'Druckt';
case 'completed': return 'Abgeschlossen';
case 'failed': return 'Fehlgeschlagen';
case 'cancelled': return 'Abgebrochen';
default: return 'Unbekannt';
}
}
// Pagination
function updatePagination() {
const totalPages = Math.ceil(filteredJobs.length / jobsPerPage);
const startIndex = (currentPage - 1) * jobsPerPage + 1;
const endIndex = Math.min(currentPage * jobsPerPage, filteredJobs.length);
document.getElementById('pagination-info').textContent =
`Zeige ${startIndex}-${endIndex} von ${filteredJobs.length} Aufträgen`;
document.getElementById('prev-page').disabled = currentPage === 1;
document.getElementById('next-page').disabled = currentPage === totalPages || totalPages === 0;
// Update page numbers
const pageNumbers = document.getElementById('page-numbers');
pageNumbers.innerHTML = '';
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
const button = document.createElement('button');
button.textContent = i;
button.onclick = () => goToPage(i);
button.className = `px-3 py-1 border rounded-lg text-sm transition-all duration-200 ${
i === currentPage
? 'bg-mercedes-blue text-white border-mercedes-blue'
: 'border-mercedes-silver hover:bg-mercedes-light'
}`;
pageNumbers.appendChild(button);
}
}
function changePage(delta) {
const totalPages = Math.ceil(filteredJobs.length / jobsPerPage);
const newPage = currentPage + delta;
if (newPage >= 1 && newPage <= totalPages) {
currentPage = newPage;
renderJobs();
updatePagination();
}
}
function goToPage(page) {
currentPage = page;
renderJobs();
updatePagination();
}
// Job actions
async function showJobDetail(jobId) {
const job = jobs.find(j => j.id === jobId);
if (!job) return;
const printer = printers.find(p => p.id === job.printer_id);
const statusColor = getJobStatusColor(job.status);
const statusText = getJobStatusText(job.status);
const content = document.getElementById('job-detail-content');
content.innerHTML = `
<div class="space-y-6">
<div class="grid grid-cols-2 gap-4">
<div>
<h3 class="font-medium text-mercedes-black mb-2">Dateiinformationen</h3>
<div class="space-y-2 text-sm">
<div><span class="font-medium text-mercedes-gray">Name:</span> ${job.filename}</div>
<div><span class="font-medium text-mercedes-gray">Größe:</span> ${formatFileSize(job.file_size)}</div>
<div><span class="font-medium text-mercedes-gray">Typ:</span> ${job.file_type || 'Unbekannt'}</div>
</div>
</div>
<div>
<h3 class="font-medium text-mercedes-black mb-2">Druckstatus</h3>
<div class="space-y-2 text-sm">
<div>
<span class="font-medium text-mercedes-gray">Status:</span>
<span class="ml-2 ${statusColor} px-2 py-1 rounded text-xs">${statusText}</span>
</div>
<div><span class="font-medium text-mercedes-gray">Fortschritt:</span> ${job.progress || 0}%</div>
<div><span class="font-medium text-mercedes-gray">Drucker:</span> ${printer ? printer.name : 'Unbekannt'}</div>
</div>
</div>
</div>
<div>
<h3 class="font-medium text-mercedes-black mb-2">Zeitstempel</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div><span class="font-medium text-mercedes-gray">Erstellt:</span> ${formatDate(job.created_at)}</div>
<div><span class="font-medium text-mercedes-gray">Gestartet:</span> ${job.started_at ? formatDate(job.started_at) : 'Noch nicht gestartet'}</div>
<div><span class="font-medium text-mercedes-gray">Beendet:</span> ${job.completed_at ? formatDate(job.completed_at) : 'Noch nicht beendet'}</div>
<div><span class="font-medium text-mercedes-gray">Geschätzte Zeit:</span> ${job.estimated_time ? formatDuration(job.estimated_time) : 'Unbekannt'}</div>
</div>
</div>
${job.error_message ? `
<div>
<h3 class="font-medium text-mercedes-red mb-2">Fehlermeldung</h3>
<div class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">
${job.error_message}
</div>
</div>
` : ''}
<div class="flex space-x-3 pt-4 border-t border-mercedes-silver">
${job.status === 'pending' ? `
<button onclick="cancelJob(${job.id}); hideJobDetailModal();"
class="bg-mercedes-red hover:bg-red-700 text-white py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
Auftrag abbrechen
</button>
` : ''}
<button onclick="hideJobDetailModal()"
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
Schließen
</button>
</div>
</div>
`;
document.getElementById('jobDetailModal').classList.remove('hidden');
}
function hideJobDetailModal() {
document.getElementById('jobDetailModal').classList.add('hidden');
}
async function cancelJob(jobId) {
if (!confirm('Sind Sie sicher, dass Sie diesen Auftrag abbrechen möchten?')) {
return;
}
try {
await apiCall(`/api/jobs/${jobId}/cancel`, {
method: 'POST'
});
showFlashMessage('Auftrag erfolgreich abgebrochen', 'success');
loadJobs();
} catch (error) {
showFlashMessage('Fehler beim Abbrechen des Auftrags', 'error');
}
}
// Refresh jobs
function refreshJobs() {
showFlashMessage('Aufträge werden aktualisiert...', 'info');
loadJobs();
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadJobs();
// Auto-refresh every 30 seconds
setInterval(loadJobs, 30000);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,384 @@
{% extends "base.html" %}
{% block title %}Anmelden - 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>
<!-- 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"
>
</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>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input
id="remember"
name="remember"
type="checkbox"
class="h-4 w-4 text-mercedes-blue focus:ring-mercedes-blue border-mercedes-silver rounded"
>
<label for="remember" class="ml-2 block text-sm text-mercedes-gray">
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>
</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"
>
← Zurück zur Anmeldung
</button>
</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>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<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" />
`;
}
}
// 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');
// Show loading state
button.disabled = true;
buttonText.textContent = 'Anmelden...';
spinner.classList.remove('hidden');
try {
const formData = new FormData(event.target);
const response = await fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: formData.get('email'),
password: formData.get('password'),
remember: formData.get('remember') === 'on'
})
});
const data = await response.json();
if (response.ok) {
showFlashMessage('Erfolgreich angemeldet!', 'success');
setTimeout(() => {
window.location.href = '/dashboard';
}, 1000);
} else {
showFlashMessage(data.error || 'Anmeldung fehlgeschlagen', 'error');
}
} catch (error) {
showFlashMessage('Netzwerkfehler bei der Anmeldung', 'error');
} finally {
// Reset button state
button.disabled = false;
buttonText.textContent = 'Anmelden';
spinner.classList.add('hidden');
}
}
// 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();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,384 @@
{% extends "base.html" %}
{% block title %}Neuer Druckauftrag - 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">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-mercedes-black">Neuer Druckauftrag</h1>
<p class="mt-2 text-mercedes-gray">Erstellen Sie einen neuen 3D-Druckauftrag</p>
</div>
<a href="/jobs" class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Zurück zu Aufträgen
</a>
</div>
</div>
<!-- Job Creation Form -->
<div class="mercedes-card rounded-xl p-8">
<form id="jobForm" onsubmit="handleJobSubmission(event)" class="space-y-8">
<!-- File Upload Section -->
<div>
<h2 class="text-xl font-bold text-mercedes-black mb-4">Datei hochladen</h2>
<div id="file-upload-area" class="border-2 border-dashed border-mercedes-silver rounded-xl p-8 text-center hover:border-mercedes-blue transition-colors duration-200 cursor-pointer">
<input type="file" id="file-input" name="file" accept=".stl,.gcode,.3mf,.obj" class="hidden" onchange="handleFileSelect(event)">
<div id="upload-placeholder" class="space-y-4">
<svg class="h-16 w-16 text-mercedes-silver mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<div>
<p class="text-lg font-medium text-mercedes-black">Datei hier ablegen oder klicken zum Auswählen</p>
<p class="text-sm text-mercedes-gray mt-2">Unterstützte Formate: STL, GCODE, 3MF, OBJ (max. 100MB)</p>
</div>
</div>
<div id="file-preview" class="hidden space-y-4">
<svg class="h-16 w-16 text-mercedes-green mx-auto" 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 id="file-name" class="text-lg font-medium text-mercedes-black"></p>
<p id="file-size" class="text-sm text-mercedes-gray"></p>
</div>
<button type="button" onclick="clearFile()" class="text-mercedes-red hover:text-red-700 text-sm transition-colors duration-200">
Datei entfernen
</button>
</div>
</div>
</div>
<!-- Job Settings Section -->
<div>
<h2 class="text-xl font-bold text-mercedes-black mb-4">Druckeinstellungen</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Printer Selection -->
<div>
<label for="printer-select" class="block text-sm font-medium text-mercedes-black mb-2">
Drucker auswählen *
</label>
<select id="printer-select" name="printer_id" 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">
<option value="">Drucker auswählen...</option>
</select>
<p class="mt-1 text-xs text-mercedes-gray">Nur verfügbare Drucker werden angezeigt</p>
</div>
<!-- Priority -->
<div>
<label for="priority-select" class="block text-sm font-medium text-mercedes-black mb-2">
Priorität
</label>
<select id="priority-select" name="priority"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="normal">Normal</option>
<option value="high">Hoch</option>
<option value="urgent">Dringend</option>
</select>
</div>
<!-- Material -->
<div>
<label for="material-input" class="block text-sm font-medium text-mercedes-black mb-2">
Material
</label>
<input type="text" id="material-input" name="material" placeholder="z.B. PLA, ABS, PETG"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<!-- Color -->
<div>
<label for="color-input" class="block text-sm font-medium text-mercedes-black mb-2">
Farbe
</label>
<input type="text" id="color-input" name="color" placeholder="z.B. Weiß, Schwarz, Rot"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<!-- Layer Height -->
<div>
<label for="layer-height-select" class="block text-sm font-medium text-mercedes-black mb-2">
Schichthöhe
</label>
<select id="layer-height-select" name="layer_height"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Standard</option>
<option value="0.1">0.1mm (Hoch)</option>
<option value="0.2">0.2mm (Normal)</option>
<option value="0.3">0.3mm (Schnell)</option>
</select>
</div>
<!-- Infill -->
<div>
<label for="infill-select" class="block text-sm font-medium text-mercedes-black mb-2">
Füllung
</label>
<select id="infill-select" name="infill"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Standard</option>
<option value="10">10% (Leicht)</option>
<option value="20">20% (Normal)</option>
<option value="50">50% (Stabil)</option>
<option value="100">100% (Massiv)</option>
</select>
</div>
</div>
</div>
<!-- Notes Section -->
<div>
<h2 class="text-xl font-bold text-mercedes-black mb-4">Zusätzliche Informationen</h2>
<div>
<label for="notes-textarea" class="block text-sm font-medium text-mercedes-black mb-2">
Notizen
</label>
<textarea id="notes-textarea" name="notes" rows="4"
placeholder="Besondere Anweisungen oder Hinweise für den Druck..."
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue"></textarea>
</div>
</div>
<!-- Submit Section -->
<div class="flex items-center justify-between pt-6 border-t border-mercedes-silver">
<div class="text-sm text-mercedes-gray">
<p>* Pflichtfelder</p>
</div>
<div class="flex space-x-4">
<button type="button" onclick="resetForm()"
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
Zurücksetzen
</button>
<button type="submit" id="submit-button" disabled
class="bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed">
<span id="submit-text">Auftrag erstellen</span>
<svg id="submit-spinner" class="hidden animate-spin h-5 w-5 inline ml-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>
</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let selectedFile = null;
let printers = [];
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadPrinters();
setupFileUpload();
});
// Load available printers
async function loadPrinters() {
try {
const response = await apiCall('/api/printers');
printers = response.printers || [];
const select = document.getElementById('printer-select');
select.innerHTML = '<option value="">Drucker auswählen...</option>';
// Only show available printers
const availablePrinters = printers.filter(p => p.status === 'available');
if (availablePrinters.length === 0) {
select.innerHTML = '<option value="">Keine verfügbaren Drucker</option>';
select.disabled = true;
showFlashMessage('Derzeit sind keine Drucker verfügbar', 'warning');
return;
}
availablePrinters.forEach(printer => {
const option = document.createElement('option');
option.value = printer.id;
option.textContent = `${printer.name} (${printer.location})`;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading printers:', error);
showFlashMessage('Fehler beim Laden der Drucker', 'error');
}
}
// Setup file upload
function setupFileUpload() {
const uploadArea = document.getElementById('file-upload-area');
const fileInput = document.getElementById('file-input');
// Click to select file
uploadArea.addEventListener('click', () => {
if (!selectedFile) {
fileInput.click();
}
});
// Drag and drop
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('border-mercedes-blue', 'bg-blue-50');
});
uploadArea.addEventListener('dragleave', (e) => {
e.preventDefault();
uploadArea.classList.remove('border-mercedes-blue', 'bg-blue-50');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('border-mercedes-blue', 'bg-blue-50');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileSelect({ target: { files } });
}
});
}
// Handle file selection
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
// Validate file type
const allowedTypes = ['.stl', '.gcode', '.3mf', '.obj'];
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
if (!allowedTypes.includes(fileExtension)) {
showFlashMessage('Ungültiger Dateityp. Erlaubt sind: STL, GCODE, 3MF, OBJ', 'error');
return;
}
// Validate file size (100MB max)
const maxSize = 100 * 1024 * 1024; // 100MB in bytes
if (file.size > maxSize) {
showFlashMessage('Datei ist zu groß. Maximum: 100MB', 'error');
return;
}
selectedFile = file;
showFilePreview(file);
validateForm();
}
// Show file preview
function showFilePreview(file) {
document.getElementById('upload-placeholder').classList.add('hidden');
document.getElementById('file-preview').classList.remove('hidden');
document.getElementById('file-name').textContent = file.name;
document.getElementById('file-size').textContent = formatFileSize(file.size);
}
// Clear selected file
function clearFile() {
selectedFile = null;
document.getElementById('file-input').value = '';
document.getElementById('upload-placeholder').classList.remove('hidden');
document.getElementById('file-preview').classList.add('hidden');
validateForm();
}
// Validate form
function validateForm() {
const printerSelected = document.getElementById('printer-select').value;
const submitButton = document.getElementById('submit-button');
const isValid = selectedFile && printerSelected;
submitButton.disabled = !isValid;
}
// Form validation on change
document.getElementById('printer-select').addEventListener('change', validateForm);
// Handle form submission
async function handleJobSubmission(event) {
event.preventDefault();
if (!selectedFile) {
showFlashMessage('Bitte wählen Sie eine Datei aus', 'error');
return;
}
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('printer_id', document.getElementById('printer-select').value);
formData.append('priority', document.getElementById('priority-select').value);
formData.append('material', document.getElementById('material-input').value);
formData.append('color', document.getElementById('color-input').value);
formData.append('layer_height', document.getElementById('layer-height-select').value);
formData.append('infill', document.getElementById('infill-select').value);
formData.append('notes', document.getElementById('notes-textarea').value);
// Show loading state
const submitButton = document.getElementById('submit-button');
const submitText = document.getElementById('submit-text');
const submitSpinner = document.getElementById('submit-spinner');
submitButton.disabled = true;
submitText.textContent = 'Wird erstellt...';
submitSpinner.classList.remove('hidden');
try {
const response = await fetch('/api/jobs', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
showFlashMessage('Druckauftrag erfolgreich erstellt', 'success');
// Redirect to job details or jobs list
setTimeout(() => {
window.location.href = `/job/${result.job_id}`;
}, 1500);
} else {
throw new Error(result.message || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Error creating job:', error);
showFlashMessage('Fehler beim Erstellen des Auftrags: ' + error.message, 'error');
// Reset button state
submitButton.disabled = false;
submitText.textContent = 'Auftrag erstellen';
submitSpinner.classList.add('hidden');
validateForm();
}
}
// Reset form
function resetForm() {
if (confirm('Sind Sie sicher, dass Sie das Formular zurücksetzen möchten?')) {
document.getElementById('jobForm').reset();
clearFile();
validateForm();
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,362 @@
{% extends "base.html" %}
{% 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">
<!-- 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>
<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">
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Drucker hinzufügen
</button>
{% endif %}
</div>
</div>
</div>
<!-- Printers Grid -->
<div id="printers-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 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>
</div>
</div>
<!-- Add Printer Modal -->
{% 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">
<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">
<div>
<label for="printer-name" class="block text-sm font-medium text-mercedes-black 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">
</div>
<div>
<label for="printer-model" class="block text-sm font-medium text-mercedes-black 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">
</div>
<div>
<label for="printer-location" class="block text-sm font-medium text-mercedes-black 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">
</div>
<div>
<label for="printer-mac" class="block text-sm font-medium text-mercedes-black 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">
</div>
<div>
<label for="printer-ip" class="block text-sm font-medium text-mercedes-black 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">
</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">
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">
Hinzufügen
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!-- 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div id="printer-detail-content">
<!-- Content will be loaded here -->
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
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
function showAddPrinterModal() {
document.getElementById('addPrinterModal').classList.remove('hidden');
}
function hideAddPrinterModal() {
document.getElementById('addPrinterModal').classList.add('hidden');
document.getElementById('addPrinterForm').reset();
}
function showPrinterDetail(printerId) {
const printer = printers.find(p => p.id === printerId);
if (!printer) return;
const content = document.getElementById('printer-detail-content');
content.innerHTML = `
<div class="space-y-4">
<div>
<h3 class="font-medium text-mercedes-black">${printer.name}</h3>
<p class="text-sm text-mercedes-gray">${printer.model}</p>
</div>
<div class="grid grid-cols-2 gap-4 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">
${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>
</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>
</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>
</div>
</div>
<div class="pt-4 border-t border-mercedes-silver">
<p class="text-xs text-mercedes-gray">
Erstellt: ${formatDate(printer.created_at)}
</p>
</div>
</div>
`;
document.getElementById('printerDetailModal').classList.remove('hidden');
}
function hidePrinterDetailModal() {
document.getElementById('printerDetailModal').classList.add('hidden');
}
// Add printer
async function handleAddPrinter(event) {
event.preventDefault();
const formData = new FormData(event.target);
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')
};
try {
await apiCall('/api/printers', {
method: 'POST',
body: JSON.stringify(printerData)
});
showFlashMessage('Drucker erfolgreich hinzugefügt', 'success');
hideAddPrinterModal();
loadPrinters();
} catch (error) {
showFlashMessage('Fehler beim Hinzufügen des Druckers', 'error');
}
}
// Delete printer
async function deletePrinter(printerId) {
if (!confirm('Sind Sie sicher, dass Sie diesen Drucker löschen möchten?')) {
return;
}
try {
await apiCall(`/api/printers/${printerId}`, {
method: 'DELETE'
});
showFlashMessage('Drucker erfolgreich gelöscht', 'success');
loadPrinters();
} catch (error) {
showFlashMessage('Fehler beim Löschen des Druckers', 'error');
}
}
// Refresh printers
function refreshPrinters() {
showFlashMessage('Drucker werden aktualisiert...', 'info');
loadPrinters();
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadPrinters();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,410 @@
{% extends "base.html" %}
{% block title %}Profil - 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-mercedes-black">Mein Profil</h1>
<p class="mt-2 text-mercedes-gray">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="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">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">
<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>
Bearbeiten
</button>
</div>
<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">
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">
</div>
<div>
<label for="last-name" class="block text-sm font-medium text-mercedes-black 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">
</div>
<div>
<label for="email" class="block text-sm font-medium text-mercedes-black 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">
</div>
<div>
<label for="phone" class="block text-sm font-medium text-mercedes-black 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">
</div>
<div>
<label for="department" class="block text-sm font-medium text-mercedes-black 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">
</div>
<div>
<label for="role" class="block text-sm font-medium text-mercedes-black 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">
</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">
Ä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">
Abbrechen
</button>
</div>
</form>
</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>
<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">
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">
</div>
<div>
<label for="new-password" class="block text-sm font-medium text-mercedes-black 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>
</div>
<div>
<label for="confirm-password" class="block text-sm font-medium text-mercedes-black 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">
</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">
Passwort ändern
</button>
</form>
</div>
</div>
<!-- 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="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>
</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>
</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>
</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>
</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="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>
</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>
</div>
<div>
<span class="text-mercedes-gray">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">
Aktiv
</span>
</div>
</div>
</div>
</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="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">
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">
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">
Daten exportieren
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let isEditMode = false;
let originalFormData = {};
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadUserStats();
storeOriginalFormData();
});
// Store original form data
function storeOriginalFormData() {
const form = document.getElementById('profile-form');
const formData = new FormData(form);
originalFormData = {};
for (let [key, value] of formData.entries()) {
originalFormData[key] = value;
}
}
// Toggle edit mode
function toggleEditMode() {
isEditMode = !isEditMode;
const inputs = document.querySelectorAll('#profile-form input:not(#role)');
const editButton = document.getElementById('edit-button');
const formActions = document.getElementById('form-actions');
inputs.forEach(input => {
input.disabled = !isEditMode;
});
if (isEditMode) {
editButton.innerHTML = `
<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="M6 18L18 6M6 6l12 12" />
</svg>
Abbrechen
`;
editButton.onclick = cancelEdit;
formActions.classList.remove('hidden');
} else {
editButton.innerHTML = `
<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>
Bearbeiten
`;
editButton.onclick = toggleEditMode;
formActions.classList.add('hidden');
}
}
// Cancel edit
function cancelEdit() {
// Restore original values
Object.keys(originalFormData).forEach(key => {
const input = document.querySelector(`[name="${key}"]`);
if (input) {
input.value = originalFormData[key];
}
});
toggleEditMode();
}
// Handle profile update
async function handleProfileUpdate(event) {
event.preventDefault();
const formData = new FormData(event.target);
const profileData = {};
for (let [key, value] of formData.entries()) {
profileData[key] = value;
}
try {
const response = await apiCall('/api/user/profile', {
method: 'PUT',
body: JSON.stringify(profileData)
});
if (response.success) {
showFlashMessage('Profil erfolgreich aktualisiert', 'success');
storeOriginalFormData();
toggleEditMode();
} else {
throw new Error(response.message || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Error updating profile:', error);
showFlashMessage('Fehler beim Aktualisieren des Profils: ' + error.message, 'error');
}
}
// Handle password change
async function handlePasswordChange(event) {
event.preventDefault();
const formData = new FormData(event.target);
const newPassword = formData.get('new_password');
const confirmPassword = formData.get('confirm_password');
if (newPassword !== confirmPassword) {
showFlashMessage('Die Passwörter stimmen nicht überein', 'error');
return;
}
const passwordData = {
current_password: formData.get('current_password'),
new_password: newPassword
};
try {
const response = await apiCall('/api/user/password', {
method: 'PUT',
body: JSON.stringify(passwordData)
});
if (response.success) {
showFlashMessage('Passwort erfolgreich geändert', 'success');
document.getElementById('password-form').reset();
} else {
throw new Error(response.message || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Error changing password:', error);
showFlashMessage('Fehler beim Ändern des Passworts: ' + error.message, 'error');
}
}
// Load user statistics
async function loadUserStats() {
try {
const response = await apiCall('/api/user/stats');
if (response.success) {
const stats = response.stats;
document.getElementById('total-jobs').textContent = stats.total_jobs || 0;
document.getElementById('completed-jobs').textContent = stats.completed_jobs || 0;
document.getElementById('active-jobs').textContent = stats.active_jobs || 0;
document.getElementById('failed-jobs').textContent = stats.failed_jobs || 0;
}
} catch (error) {
console.error('Error loading user stats:', error);
// Don't show error message for stats, just keep the dashes
}
}
// Download user data
async function downloadUserData() {
try {
const response = await fetch('/api/user/export', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'meine_daten.json';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showFlashMessage('Daten erfolgreich exportiert', 'success');
} catch (error) {
console.error('Error downloading user data:', error);
showFlashMessage('Fehler beim Exportieren der Daten', 'error');
}
}
// Password confirmation validation
document.getElementById('confirm-password').addEventListener('input', function() {
const newPassword = document.getElementById('new-password').value;
const confirmPassword = this.value;
if (confirmPassword && newPassword !== confirmPassword) {
this.setCustomValidity('Die Passwörter stimmen nicht überein');
} else {
this.setCustomValidity('');
}
});
</script>
{% endblock %}