"""
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:
parent
ead75ae451
commit
62e131c02f
@ -7,7 +7,7 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Dict, List, Optional, Tuple, Any, Union
|
from typing import Dict, List, Optional, Tuple, Any, Union
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import Flask, request, jsonify, session
|
from flask import Flask, request, jsonify, session, render_template, redirect, url_for
|
||||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
|
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
|
||||||
import sqlalchemy.exc
|
import sqlalchemy.exc
|
||||||
from PyP100 import PyP110
|
from PyP100 import PyP110
|
||||||
@ -972,6 +972,81 @@ def stop_scheduler_api():
|
|||||||
"running": scheduler.is_running()
|
"running": scheduler.is_running()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Frontend-Routen
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
"""Hauptseite - Weiterleitung zum Dashboard oder Login."""
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
return redirect(url_for('login_page'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/login")
|
||||||
|
def login_page():
|
||||||
|
"""Login-Seite."""
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/dashboard")
|
||||||
|
@login_required
|
||||||
|
def dashboard():
|
||||||
|
"""Dashboard-Seite."""
|
||||||
|
return render_template('dashboard.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/printers")
|
||||||
|
@login_required
|
||||||
|
def printers_page():
|
||||||
|
"""Drucker-Übersichtsseite."""
|
||||||
|
return render_template('printers.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/jobs")
|
||||||
|
@login_required
|
||||||
|
def jobs_page():
|
||||||
|
"""Jobs-Übersichtsseite."""
|
||||||
|
return render_template('jobs.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/jobs/new")
|
||||||
|
@login_required
|
||||||
|
def new_job_page():
|
||||||
|
"""Neuen Job erstellen."""
|
||||||
|
return render_template('job_create.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/jobs/<int:job_id>")
|
||||||
|
@login_required
|
||||||
|
@job_owner_required
|
||||||
|
def job_detail_page(job_id):
|
||||||
|
"""Job-Detailseite."""
|
||||||
|
return render_template('job_detail.html', job_id=job_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/stats")
|
||||||
|
@login_required
|
||||||
|
def stats_page():
|
||||||
|
"""Statistiken-Seite."""
|
||||||
|
return render_template('stats.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin")
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def admin_page():
|
||||||
|
"""Admin-Panel."""
|
||||||
|
return render_template('admin.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/profile")
|
||||||
|
@login_required
|
||||||
|
def profile_page():
|
||||||
|
"""Benutzerprofil."""
|
||||||
|
return render_template('profile.html')
|
||||||
|
|
||||||
|
|
||||||
# Scheduler starten
|
# Scheduler starten
|
||||||
def start_scheduler():
|
def start_scheduler():
|
||||||
"""Initialisiert und startet den Scheduler mit den erforderlichen Tasks."""
|
"""Initialisiert und startet den Scheduler mit den erforderlichen Tasks."""
|
||||||
|
65
backend/app/static/css/tailwind.min.css
vendored
Normal file
65
backend/app/static/css/tailwind.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
13
backend/app/static/mercedes.svg
Normal file
13
backend/app/static/mercedes.svg
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" version="1.1" id="svg3544" xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 80 80"
|
||||||
|
xml:space="preserve" width="800px" height="800px">
|
||||||
|
<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>
|
After Width: | Height: | Size: 976 B |
504
backend/app/templates/base.html
Normal file
504
backend/app/templates/base.html
Normal 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>
|
461
backend/app/templates/dashboard.html
Normal file
461
backend/app/templates/dashboard.html
Normal 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 %}
|
469
backend/app/templates/jobs.html
Normal file
469
backend/app/templates/jobs.html
Normal 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 %}
|
384
backend/app/templates/login.html
Normal file
384
backend/app/templates/login.html
Normal 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 %}
|
384
backend/app/templates/new_job.html
Normal file
384
backend/app/templates/new_job.html
Normal 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 %}
|
362
backend/app/templates/printers.html
Normal file
362
backend/app/templates/printers.html
Normal 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 %}
|
410
backend/app/templates/profile.html
Normal file
410
backend/app/templates/profile.html
Normal 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 %}
|
@ -9,7 +9,7 @@ Flask-Login==0.6.3
|
|||||||
SQLAlchemy==2.0.23
|
SQLAlchemy==2.0.23
|
||||||
|
|
||||||
# Smart Plug Steuerung
|
# Smart Plug Steuerung
|
||||||
PyP100==0.1.4
|
PyP100==0.1.2
|
||||||
|
|
||||||
# Passwort-Hashing (bereits in Flask enthalten, aber explizit für Klarheit)
|
# Passwort-Hashing (bereits in Flask enthalten, aber explizit für Klarheit)
|
||||||
Werkzeug==3.0.1
|
Werkzeug==3.0.1
|
||||||
|
157
frontend/check-backend-connection.sh
Executable file
157
frontend/check-backend-connection.sh
Executable file
@ -0,0 +1,157 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🔍 MYP Frontend - Backend-Verbindung prüfen
|
||||||
|
# Entwicklungsumgebung - Überprüft Verbindung zum Raspberry Pi Backend auf 192.168.0.105:5000
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Farben für Terminal-Ausgabe
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Funktionen
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Arbeitsverzeichnis setzen
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
echo "🔍 MYP Frontend - Backend-Verbindung wird überprüft..."
|
||||||
|
echo "🍓 Entwicklungsumgebung - Raspberry Pi Backend"
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
|
# Backend-URL definieren (Hardcoded für Raspberry Pi)
|
||||||
|
BACKEND_URL="http://192.168.0.105:5000"
|
||||||
|
BACKEND_API_URL="$BACKEND_URL/api"
|
||||||
|
RASPBERRY_PI_HOST="192.168.0.105"
|
||||||
|
|
||||||
|
log_info "Ziel-Backend: $BACKEND_URL (Raspberry Pi)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Netzwerk-Konnektivität zum Raspberry Pi prüfen
|
||||||
|
log_info "1. Prüfe Netzwerk-Konnektivität zum Raspberry Pi ($RASPBERRY_PI_HOST)..."
|
||||||
|
if ping -c 1 -W 3 $RASPBERRY_PI_HOST >/dev/null 2>&1; then
|
||||||
|
log_success "✓ Ping zum Raspberry Pi erfolgreich"
|
||||||
|
else
|
||||||
|
log_error "✗ Ping zum Raspberry Pi fehlgeschlagen"
|
||||||
|
log_error " Stellen Sie sicher, dass der Raspberry Pi erreichbar ist"
|
||||||
|
log_error " Prüfen Sie die Netzwerkverbindung und IP-Adresse"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Backend-Service auf Raspberry Pi prüfen
|
||||||
|
log_info "2. Prüfe Backend-Service auf Raspberry Pi Port 5000..."
|
||||||
|
if curl -f --connect-timeout 5 "$BACKEND_URL/health" >/dev/null 2>&1; then
|
||||||
|
log_success "✓ Raspberry Pi Backend-Health-Check erfolgreich"
|
||||||
|
elif curl -f --connect-timeout 5 "$BACKEND_URL" >/dev/null 2>&1; then
|
||||||
|
log_warning "⚠ Raspberry Pi Backend erreichbar, aber kein Health-Endpoint"
|
||||||
|
else
|
||||||
|
log_error "✗ Raspberry Pi Backend-Service nicht erreichbar"
|
||||||
|
log_error " Stellen Sie sicher, dass das Backend auf dem Raspberry Pi läuft"
|
||||||
|
log_error " Prüfen Sie: ssh pi@$RASPBERRY_PI_HOST 'sudo systemctl status myp-backend'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. API-Endpunkte auf Raspberry Pi prüfen
|
||||||
|
log_info "3. Prüfe Raspberry Pi Backend-API-Endpunkte..."
|
||||||
|
for endpoint in "printers" "jobs" "users"; do
|
||||||
|
if curl -f --connect-timeout 5 "$BACKEND_API_URL/$endpoint" >/dev/null 2>&1; then
|
||||||
|
log_success "✓ API-Endpunkt /$endpoint auf Raspberry Pi erreichbar"
|
||||||
|
else
|
||||||
|
log_warning "⚠ API-Endpunkt /$endpoint auf Raspberry Pi nicht erreichbar"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. Frontend-Konfigurationsdateien für Entwicklung prüfen
|
||||||
|
log_info "4. Prüfe Frontend-Konfigurationsdateien (Entwicklungsumgebung)..."
|
||||||
|
|
||||||
|
# .env.local prüfen
|
||||||
|
if [ -f ".env.local" ]; then
|
||||||
|
if grep -q "NEXT_PUBLIC_API_URL=http://192.168.0.105:5000" .env.local; then
|
||||||
|
log_success "✓ .env.local korrekt für Raspberry Pi konfiguriert"
|
||||||
|
else
|
||||||
|
log_warning "⚠ .env.local existiert, aber Raspberry Pi Backend-URL ist falsch"
|
||||||
|
log_info " Führen Sie './setup-backend-url.sh' aus"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "⚠ .env.local nicht gefunden"
|
||||||
|
log_info " Führen Sie './setup-backend-url.sh' aus"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# env.frontend prüfen
|
||||||
|
if grep -q "NODE_ENV=development" env.frontend && grep -q "NEXT_PUBLIC_API_URL=http://192.168.0.105:5000" env.frontend; then
|
||||||
|
log_success "✓ env.frontend korrekt für Entwicklungsumgebung konfiguriert"
|
||||||
|
else
|
||||||
|
log_error "✗ env.frontend nicht für Entwicklungsumgebung konfiguriert"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# api-config.ts prüfen
|
||||||
|
if grep -q 'API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://192.168.0.105:5000"' src/utils/api-config.ts; then
|
||||||
|
log_success "✓ api-config.ts korrekt für Raspberry Pi konfiguriert"
|
||||||
|
else
|
||||||
|
log_error "✗ api-config.ts hat falsche Standard-URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker-Compose-Dateien für Entwicklung prüfen
|
||||||
|
if grep -q "NODE_ENV=development" docker-compose.yml && grep -q "NEXT_PUBLIC_API_URL=http://192.168.0.105:5000" docker-compose.yml; then
|
||||||
|
log_success "✓ docker-compose.yml korrekt für Entwicklungsumgebung konfiguriert"
|
||||||
|
else
|
||||||
|
log_warning "⚠ docker-compose.yml nicht für Entwicklungsumgebung konfiguriert"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Caddy-Konfiguration für Entwicklung prüfen
|
||||||
|
if grep -q "reverse_proxy 192.168.0.105:5000" docker/caddy/Caddyfile && grep -q "localhost" docker/caddy/Caddyfile; then
|
||||||
|
log_success "✓ Caddy-Konfiguration korrekt für Entwicklungsumgebung"
|
||||||
|
else
|
||||||
|
log_error "✗ Caddy-Konfiguration nicht für Entwicklungsumgebung konfiguriert"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 5. Zusammenfassung und Empfehlungen für Entwicklungsumgebung
|
||||||
|
log_info "5. Zusammenfassung und Empfehlungen (Entwicklungsumgebung):"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ping -c 1 -W 3 $RASPBERRY_PI_HOST >/dev/null 2>&1 && curl -f --connect-timeout 5 "$BACKEND_URL" >/dev/null 2>&1; then
|
||||||
|
log_success "🎉 Raspberry Pi Backend ist erreichbar und läuft!"
|
||||||
|
echo ""
|
||||||
|
log_info "Nächste Schritte (Entwicklung):"
|
||||||
|
echo " 1. Frontend starten: ./start-frontend-server.sh"
|
||||||
|
echo " 2. Frontend testen: http://localhost:3000"
|
||||||
|
echo " 3. Health-Check: http://localhost:3000/health"
|
||||||
|
echo " 4. Backend-Health: http://localhost:3000/backend-health"
|
||||||
|
else
|
||||||
|
log_error "❌ Raspberry Pi Backend ist nicht erreichbar!"
|
||||||
|
echo ""
|
||||||
|
log_info "Fehlerbehebung (Entwicklungsumgebung):"
|
||||||
|
echo " 1. Prüfen Sie, ob der Raspberry Pi ($RASPBERRY_PI_HOST) läuft"
|
||||||
|
echo " 2. SSH zum Raspberry Pi: ssh pi@$RASPBERRY_PI_HOST"
|
||||||
|
echo " 3. Backend-Status prüfen: sudo systemctl status myp-backend"
|
||||||
|
echo " 4. Backend-Logs prüfen: sudo journalctl -u myp-backend -f"
|
||||||
|
echo " 5. Netzwerk-Konnektivität prüfen"
|
||||||
|
echo " 6. Firewall-Einstellungen auf Raspberry Pi prüfen"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Debug-Tools:"
|
||||||
|
echo " - Debug-Server: ./debug-server/start-debug-server.sh"
|
||||||
|
echo " - Backend direkt: curl http://192.168.0.105:5000/health"
|
||||||
|
echo " - SSH zum Raspberry Pi: ssh pi@192.168.0.105"
|
||||||
|
echo "=================================================="
|
@ -1,5 +1,5 @@
|
|||||||
# 🎨 MYP Frontend - Standalone Server Konfiguration
|
# 🎨 MYP Frontend - Entwicklungsumgebung Konfiguration
|
||||||
# Frontend-Service als vollständig unabhängiger Server
|
# Frontend-Service für die Entwicklung mit Raspberry Pi Backend
|
||||||
|
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
@ -8,33 +8,38 @@ services:
|
|||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.dev
|
||||||
args:
|
args:
|
||||||
- BUILDKIT_INLINE_CACHE=1
|
- BUILDKIT_INLINE_CACHE=1
|
||||||
- NODE_ENV=${NODE_ENV:-production}
|
- NODE_ENV=development
|
||||||
image: myp/frontend:latest
|
image: myp/frontend:dev
|
||||||
container_name: myp-frontend-standalone
|
container_name: myp-frontend-dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=${NODE_ENV:-production}
|
- NODE_ENV=development
|
||||||
- NEXT_TELEMETRY_DISABLED=1
|
- NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
# Backend API Konfiguration
|
# Backend API Konfiguration (Raspberry Pi)
|
||||||
- NEXT_PUBLIC_API_URL=${BACKEND_API_URL:-http://localhost:5000/api}
|
- NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
|
||||||
- NEXT_PUBLIC_BACKEND_HOST=${BACKEND_HOST:-localhost:5000}
|
- NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
|
||||||
|
|
||||||
# Frontend Server
|
# Frontend Server
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
- HOSTNAME=0.0.0.0
|
- HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
# Auth Konfiguration
|
# Auth Konfiguration (Entwicklung)
|
||||||
- NEXTAUTH_URL=${FRONTEND_URL:-http://localhost:3000}
|
- NEXTAUTH_URL=http://localhost:3000
|
||||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-frontend-auth-secret}
|
- NEXTAUTH_SECRET=dev-frontend-auth-secret
|
||||||
|
|
||||||
|
# Debug-Einstellungen
|
||||||
|
- DEBUG=true
|
||||||
|
- NEXT_DEBUG=true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- frontend_data:/app/.next
|
- .:/app
|
||||||
- frontend_cache:/app/.next/cache
|
- /app/node_modules
|
||||||
|
- /app/.next
|
||||||
- ./public:/app/public:ro
|
- ./public:/app/public:ro
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
@ -43,6 +48,9 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- frontend-network
|
- frontend-network
|
||||||
|
|
||||||
|
extra_hosts:
|
||||||
|
- "raspberrypi:192.168.0.105"
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -52,8 +60,8 @@ services:
|
|||||||
|
|
||||||
labels:
|
labels:
|
||||||
- "service.type=frontend"
|
- "service.type=frontend"
|
||||||
- "service.name=myp-frontend"
|
- "service.name=myp-frontend-dev"
|
||||||
- "service.environment=${NODE_ENV:-production}"
|
- "service.environment=development"
|
||||||
|
|
||||||
# === FRONTEND CACHE (Optional: Redis für Session Management) ===
|
# === FRONTEND CACHE (Optional: Redis für Session Management) ===
|
||||||
frontend-cache:
|
frontend-cache:
|
||||||
|
@ -5,25 +5,34 @@ services:
|
|||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.dev
|
||||||
container_name: myp-rp
|
container_name: myp-rp-dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=development
|
||||||
- NEXT_PUBLIC_API_URL=https://m040tbaraspi001.de040.corpintra.net/api
|
- NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
|
||||||
|
- NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
|
||||||
|
- DEBUG=true
|
||||||
|
- NEXT_DEBUG=true
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
- /app/.next
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
networks:
|
networks:
|
||||||
- myp-network
|
- myp-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
|
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
# Caddy Proxy
|
# Caddy Proxy (Entwicklung)
|
||||||
caddy:
|
caddy:
|
||||||
image: caddy:2.7-alpine
|
image: caddy:2.7-alpine
|
||||||
container_name: myp-caddy
|
container_name: myp-caddy-dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
@ -36,9 +45,10 @@ services:
|
|||||||
- myp-network
|
- myp-network
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
- "raspberrypi:192.168.0.105"
|
||||||
environment:
|
environment:
|
||||||
- CADDY_HOST=53.37.211.254
|
- CADDY_HOST=localhost
|
||||||
- CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net
|
- CADDY_DOMAIN=localhost
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
|
|
||||||
|
@ -2,51 +2,54 @@
|
|||||||
debug
|
debug
|
||||||
}
|
}
|
||||||
|
|
||||||
# Hauptdomain und IP-Adresse für die Anwendung
|
# Entwicklungsumgebung - Localhost und Raspberry Pi Backend
|
||||||
53.37.211.254, m040tbaraspi001.de040.corpintra.net, m040tbaraspi001, de040.corpintra.net, localhost {
|
localhost, 127.0.0.1 {
|
||||||
# API Anfragen zum Backend weiterleiten
|
# API Anfragen zum Raspberry Pi Backend weiterleiten
|
||||||
@api {
|
@api {
|
||||||
path /api/* /health
|
path /api/* /health
|
||||||
}
|
}
|
||||||
handle @api {
|
handle @api {
|
||||||
uri strip_prefix /api
|
uri strip_prefix /api
|
||||||
reverse_proxy 192.168.0.5:5000
|
reverse_proxy 192.168.0.105:5000 {
|
||||||
|
header_up Host {upstream_hostport}
|
||||||
|
header_up X-Real-IP {remote_host}
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Alle anderen Anfragen zum Frontend weiterleiten
|
# Alle anderen Anfragen zum Frontend weiterleiten
|
||||||
handle {
|
handle {
|
||||||
reverse_proxy myp-rp:3000
|
reverse_proxy myp-rp-dev:3000 {
|
||||||
|
header_up Host {upstream_hostport}
|
||||||
|
header_up X-Real-IP {remote_host}
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tls internal {
|
# TLS für Entwicklung deaktiviert
|
||||||
on_demand
|
tls off
|
||||||
}
|
|
||||||
|
|
||||||
# Erlaube HTTP -> HTTPS Redirects für OAuth
|
# OAuth Callbacks für Entwicklung
|
||||||
@oauth path /auth/login/callback*
|
@oauth path /auth/login/callback*
|
||||||
handle @oauth {
|
handle @oauth {
|
||||||
header Cache-Control "no-cache"
|
header Cache-Control "no-cache"
|
||||||
reverse_proxy myp-rp:3000
|
reverse_proxy myp-rp-dev:3000
|
||||||
}
|
}
|
||||||
|
|
||||||
# Allgemeine Header für Sicherheit und Caching
|
# Entwicklungsfreundliche Header
|
||||||
header {
|
header {
|
||||||
# Sicherheitsheader
|
# Weniger restriktive Sicherheitsheader für Entwicklung
|
||||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
||||||
X-Content-Type-Options "nosniff"
|
X-Content-Type-Options "nosniff"
|
||||||
X-Frame-Options "SAMEORIGIN"
|
X-Frame-Options "SAMEORIGIN"
|
||||||
Referrer-Policy "strict-origin-when-cross-origin"
|
|
||||||
|
|
||||||
# Cache-Control für statische Assets
|
# Keine Caches für Entwicklung
|
||||||
@static {
|
Cache-Control "no-store, no-cache, must-revalidate"
|
||||||
path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
|
||||||
}
|
|
||||||
header @static Cache-Control "public, max-age=86400"
|
|
||||||
|
|
||||||
# Keine Caches für dynamische Inhalte
|
# CORS für Entwicklung
|
||||||
@dynamic {
|
Access-Control-Allow-Origin "*"
|
||||||
not path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||||
}
|
Access-Control-Allow-Headers "Content-Type, Authorization"
|
||||||
header @dynamic Cache-Control "no-store, no-cache, must-revalidate"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,8 @@
|
|||||||
# 🎨 MYP Frontend - Standalone Server Konfiguration
|
# 🎨 MYP Frontend - Entwicklungsumgebung Konfiguration
|
||||||
# Umgebungsvariablen ausschließlich für den Frontend-Server
|
# Umgebungsvariablen für die Verbindung zum Raspberry Pi Backend
|
||||||
|
|
||||||
# === NODE.JS KONFIGURATION ===
|
# === NODE.JS KONFIGURATION ===
|
||||||
NODE_ENV=production
|
NODE_ENV=development
|
||||||
NEXT_TELEMETRY_DISABLED=1
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
# === FRONTEND SERVER ===
|
# === FRONTEND SERVER ===
|
||||||
@ -11,18 +11,18 @@ HOSTNAME=0.0.0.0
|
|||||||
FRONTEND_URL=http://localhost:3000
|
FRONTEND_URL=http://localhost:3000
|
||||||
|
|
||||||
# === BACKEND API KONFIGURATION ===
|
# === BACKEND API KONFIGURATION ===
|
||||||
# Backend-Server Verbindung (HTTP)
|
# Backend-Server Verbindung (Raspberry Pi)
|
||||||
BACKEND_API_URL=http://localhost:5000/api
|
BACKEND_API_URL=http://192.168.0.105:5000/api
|
||||||
BACKEND_HOST=localhost:5000
|
BACKEND_HOST=192.168.0.105:5000
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:5000/api
|
NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
|
||||||
NEXT_PUBLIC_BACKEND_HOST=localhost:5000
|
NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
|
||||||
|
|
||||||
# === AUTHENTIFIZIERUNG ===
|
# === AUTHENTIFIZIERUNG ===
|
||||||
NEXTAUTH_URL=http://localhost:3000
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
NEXTAUTH_SECRET=frontend-auth-secret-2024
|
NEXTAUTH_SECRET=dev-frontend-auth-secret-2024
|
||||||
JWT_SECRET=frontend-jwt-secret-2024
|
JWT_SECRET=dev-frontend-jwt-secret-2024
|
||||||
|
|
||||||
# OAuth Provider (falls verwendet)
|
# OAuth Provider (Entwicklung)
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
MICROSOFT_CLIENT_ID=
|
MICROSOFT_CLIENT_ID=
|
||||||
@ -35,7 +35,7 @@ FRONTEND_DB_PATH=db/frontend.db
|
|||||||
|
|
||||||
# === CACHE KONFIGURATION ===
|
# === CACHE KONFIGURATION ===
|
||||||
# Frontend-spezifischer Redis Cache (separater Port!)
|
# Frontend-spezifischer Redis Cache (separater Port!)
|
||||||
FRONTEND_REDIS_PASSWORD=frontend_cache_password
|
FRONTEND_REDIS_PASSWORD=dev_frontend_cache_password
|
||||||
FRONTEND_REDIS_HOST=localhost
|
FRONTEND_REDIS_HOST=localhost
|
||||||
FRONTEND_REDIS_PORT=6380
|
FRONTEND_REDIS_PORT=6380
|
||||||
FRONTEND_REDIS_DB=1
|
FRONTEND_REDIS_DB=1
|
||||||
@ -45,19 +45,19 @@ CDN_URL=http://localhost:8080
|
|||||||
ASSETS_URL=http://localhost:8080/static
|
ASSETS_URL=http://localhost:8080/static
|
||||||
|
|
||||||
# === SICHERHEIT ===
|
# === SICHERHEIT ===
|
||||||
# CSP (Content Security Policy)
|
# CSP (Content Security Policy) - Entwicklung
|
||||||
CSP_SCRIPT_SRC="'self' 'unsafe-inline' 'unsafe-eval'"
|
CSP_SCRIPT_SRC="'self' 'unsafe-inline' 'unsafe-eval'"
|
||||||
CSP_STYLE_SRC="'self' 'unsafe-inline'"
|
CSP_STYLE_SRC="'self' 'unsafe-inline'"
|
||||||
CSP_IMG_SRC="'self' data: https:"
|
CSP_IMG_SRC="'self' data: https:"
|
||||||
CSP_CONNECT_SRC="'self' ws: wss: http://localhost:5000"
|
CSP_CONNECT_SRC="'self' ws: wss: http://192.168.0.105:5000 http://localhost:5000"
|
||||||
|
|
||||||
# === MONITORING ===
|
# === MONITORING ===
|
||||||
ANALYTICS_ENABLED=true
|
ANALYTICS_ENABLED=false
|
||||||
ERROR_REPORTING_ENABLED=true
|
ERROR_REPORTING_ENABLED=true
|
||||||
|
|
||||||
# === ENTWICKLUNG ===
|
# === ENTWICKLUNG ===
|
||||||
DEBUG=false
|
DEBUG=true
|
||||||
NEXT_DEBUG=false
|
NEXT_DEBUG=true
|
||||||
|
|
||||||
# === BUILD KONFIGURATION ===
|
# === BUILD KONFIGURATION ===
|
||||||
ANALYZE=false
|
ANALYZE=false
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
// Entwicklungsumgebung - weniger restriktive CORS
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -7,7 +8,7 @@ const nextConfig = {
|
|||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Access-Control-Allow-Origin",
|
key: "Access-Control-Allow-Origin",
|
||||||
value: "m040tbaraspi001.de040.corpintra.net",
|
value: "*",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Access-Control-Allow-Methods",
|
key: "Access-Control-Allow-Methods",
|
||||||
@ -21,6 +22,30 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
// Rewrites für Backend-API-Aufrufe zum Raspberry Pi
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/api/backend/:path*',
|
||||||
|
destination: 'http://192.168.0.105:5000/api/:path*',
|
||||||
|
},
|
||||||
|
// Direkter Proxy für Health-Checks
|
||||||
|
{
|
||||||
|
source: '/backend-health',
|
||||||
|
destination: 'http://192.168.0.105:5000/health',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
// Entwicklungseinstellungen
|
||||||
|
experimental: {
|
||||||
|
serverComponentsExternalPackages: [],
|
||||||
|
},
|
||||||
|
// Logging für Entwicklung
|
||||||
|
logging: {
|
||||||
|
fetches: {
|
||||||
|
fullUrl: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
66
frontend/setup-backend-url.sh
Normal file → Executable file
66
frontend/setup-backend-url.sh
Normal file → Executable file
@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Skript zum Setzen der Backend-URL in der Frontend-Konfiguration
|
# Skript zum Setzen der Backend-URL in der Frontend-Konfiguration
|
||||||
# Verwendet für die Verbindung zum Backend-Server unter 192.168.0.105:5000
|
# Entwicklungsumgebung - Raspberry Pi Backend auf 192.168.0.105:5000
|
||||||
|
|
||||||
# Farbcodes für Ausgabe
|
# Farbcodes für Ausgabe
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@ -22,59 +22,57 @@ error_log() {
|
|||||||
# Definiere Variablen
|
# Definiere Variablen
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
ENV_FILE="$SCRIPT_DIR/.env.local"
|
ENV_FILE="$SCRIPT_DIR/.env.local"
|
||||||
DEFAULT_BACKEND_URL="http://192.168.0.105:5000"
|
# Hardcoded für Entwicklungsumgebung - Raspberry Pi Backend
|
||||||
|
BACKEND_URL="http://192.168.0.105:5000"
|
||||||
|
|
||||||
# Falls übergebene Parameter vorhanden sind, Backend-URL anpassen
|
log "Entwicklungsumgebung - Raspberry Pi Backend: ${BACKEND_URL}"
|
||||||
if [ -n "$1" ]; then
|
|
||||||
BACKEND_URL="$1"
|
|
||||||
log "Verwende übergebene Backend-URL: ${BACKEND_URL}"
|
|
||||||
else
|
|
||||||
BACKEND_URL="$DEFAULT_BACKEND_URL"
|
|
||||||
log "Verwende Standard-Backend-URL: ${BACKEND_URL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Bestimme den Hostnamen für OAuth
|
# Hostname für OAuth (Entwicklung)
|
||||||
HOSTNAME=$(hostname)
|
FRONTEND_HOSTNAME="localhost"
|
||||||
if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then
|
OAUTH_URL="http://localhost:3000/auth/login/callback"
|
||||||
FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
log "Frontend-Hostname (Entwicklung): $FRONTEND_HOSTNAME"
|
||||||
OAUTH_URL="http://m040tbaraspi001.de040.corpintra.net/auth/login/callback"
|
|
||||||
log "Erkannt: Unternehmens-Hostname: $FRONTEND_HOSTNAME"
|
|
||||||
else
|
|
||||||
FRONTEND_HOSTNAME="$HOSTNAME"
|
|
||||||
OAUTH_URL="http://$HOSTNAME:3000/auth/login/callback"
|
|
||||||
log "Lokaler Hostname: $FRONTEND_HOSTNAME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Erstelle .env.local Datei mit Backend-URL
|
# Erstelle .env.local Datei mit Backend-URL
|
||||||
log "${YELLOW}Erstelle .env.local Datei...${NC}"
|
log "${YELLOW}Erstelle .env.local Datei für Entwicklungsumgebung...${NC}"
|
||||||
cat > "$ENV_FILE" << EOL
|
cat > "$ENV_FILE" << EOL
|
||||||
# Backend API Konfiguration
|
# Backend API Konfiguration - Raspberry Pi (Entwicklung)
|
||||||
NEXT_PUBLIC_API_URL=${BACKEND_URL}
|
NEXT_PUBLIC_API_URL=${BACKEND_URL}
|
||||||
|
|
||||||
# Frontend-URL für OAuth Callback
|
# Frontend-URL für OAuth Callback (Entwicklung)
|
||||||
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}
|
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}:3000
|
||||||
|
|
||||||
# Explizite OAuth Callback URL für GitHub
|
# Explizite OAuth Callback URL für GitHub (Entwicklung)
|
||||||
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
|
||||||
|
|
||||||
# OAuth Konfiguration (falls nötig)
|
# Entwicklungsumgebung
|
||||||
OAUTH_CLIENT_ID=client_id
|
NODE_ENV=development
|
||||||
OAUTH_CLIENT_SECRET=client_secret
|
DEBUG=true
|
||||||
|
NEXT_DEBUG=true
|
||||||
|
|
||||||
|
# OAuth Konfiguration (Entwicklung)
|
||||||
|
OAUTH_CLIENT_ID=dev_client_id
|
||||||
|
OAUTH_CLIENT_SECRET=dev_client_secret
|
||||||
|
|
||||||
|
# Raspberry Pi Backend Host
|
||||||
|
NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
|
||||||
EOL
|
EOL
|
||||||
|
|
||||||
# Überprüfe, ob die Datei erstellt wurde
|
# Überprüfe, ob die Datei erstellt wurde
|
||||||
if [ -f "$ENV_FILE" ]; then
|
if [ -f "$ENV_FILE" ]; then
|
||||||
log "${GREEN}Erfolgreich .env.local Datei mit Backend-URL erstellt: ${BACKEND_URL}${NC}"
|
log "${GREEN}Erfolgreich .env.local Datei für Entwicklungsumgebung erstellt: ${BACKEND_URL}${NC}"
|
||||||
else
|
else
|
||||||
error_log "Konnte .env.local Datei nicht erstellen."
|
error_log "Konnte .env.local Datei nicht erstellen."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Hinweis für Docker-Installation
|
# Hinweis für Entwicklungsumgebung
|
||||||
log "${YELLOW}WICHTIG: Wenn Sie Docker verwenden, stellen Sie sicher, dass Sie die Umgebungsvariable setzen:${NC}"
|
log "${YELLOW}ENTWICKLUNGSUMGEBUNG KONFIGURIERT:${NC}"
|
||||||
log "NEXT_PUBLIC_API_URL=${BACKEND_URL}"
|
log "Backend (Raspberry Pi): ${BACKEND_URL}"
|
||||||
|
log "Frontend: http://localhost:3000"
|
||||||
|
log "OAuth Callback: ${OAUTH_URL}"
|
||||||
log ""
|
log ""
|
||||||
log "${GREEN}Backend-URL wurde erfolgreich konfiguriert. Nach einem Neustart der Anwendung sollte die Verbindung hergestellt werden.${NC}"
|
log "${GREEN}Backend-URL wurde erfolgreich für die Entwicklungsumgebung konfiguriert.${NC}"
|
||||||
|
log "${GREEN}Das Frontend verbindet sich jetzt mit dem Raspberry Pi Backend.${NC}"
|
||||||
|
|
||||||
# Berechtigungen setzen
|
# Berechtigungen setzen
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
|
@ -7,7 +7,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Prüfe Backend-Verbindung
|
// Prüfe Backend-Verbindung
|
||||||
const backendUrl = process.env.BACKEND_API_URL || 'http://localhost:5000';
|
const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://192.168.0.105:5000';
|
||||||
let backendStatus = 'unknown';
|
let backendStatus = 'unknown';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user