diff --git a/backend/app.py b/backend/app.py old mode 100644 new mode 100755 index 56a02af..52b8f1e --- a/backend/app.py +++ b/backend/app.py @@ -1,4 +1,4 @@ -from flask import Flask, request, jsonify, g, redirect, url_for, session as flask_session +from flask import Flask, request, jsonify, g, redirect, url_for, session as flask_session, render_template, flash from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate @@ -725,6 +725,66 @@ def server_error(error): app.logger.error(f'Serverfehler: {error}') return jsonify({'message': 'Interner Serverfehler!'}), 500 +# Web UI Routen +@app.route('/') +def index(): + current_user = get_current_user() + if current_user: + return render_template('dashboard.html', current_user=current_user, active_page='home') + return redirect(url_for('login_page')) + +@app.route('/login') +def login_page(): + return render_template('login.html', active_page='login') + +@app.route('/register') +def register_page(): + return render_template('register.html', active_page='register') + +@app.route('/logout') +def logout_page(): + session_id = flask_session.get('session_id') + if session_id: + session = Session.query.get(session_id) + if session: + db.session.delete(session) + db.session.commit() + + flask_session.pop('session_id', None) + + flash('Sie wurden erfolgreich abgemeldet.', 'success') + return redirect(url_for('login_page')) + +@app.route('/admin/printers') +def printers_page(): + current_user = get_current_user() + if not current_user: + return redirect(url_for('login_page')) + return render_template('printers.html', current_user=current_user, active_page='printers') + +@app.route('/admin/jobs') +def jobs_page(): + current_user = get_current_user() + if not current_user: + return redirect(url_for('login_page')) + return render_template('jobs.html', current_user=current_user, active_page='jobs') + +@app.route('/admin/users') +def users_page(): + current_user = get_current_user() + if not current_user or current_user.role != 'admin': + flash('Sie haben keine Berechtigung, diese Seite zu besuchen.', 'danger') + return redirect(url_for('index')) + return render_template('users.html', current_user=current_user, active_page='users') + +@app.route('/admin/stats') +def stats_page(): + current_user = get_current_user() + if not current_user or current_user.role != 'admin': + flash('Sie haben keine Berechtigung, diese Seite zu besuchen.', 'danger') + return redirect(url_for('index')) + return render_template('stats.html', current_user=current_user, active_page='stats') + # Server starten if __name__ == '__main__': app.run(debug=True, host='0.0.0.0') \ No newline at end of file diff --git a/backend/templates/base.html b/backend/templates/base.html new file mode 100644 index 0000000..23588dd --- /dev/null +++ b/backend/templates/base.html @@ -0,0 +1,169 @@ +<!DOCTYPE html> +<html lang="de"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{% block title %}MYP API Tester{% endblock %}</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"> + <style> + .sidebar { + min-height: calc(100vh - 56px); + background-color: #f8f9fa; + } + .api-response { + max-height: 300px; + overflow-y: auto; + font-family: monospace; + background-color: #f5f5f5; + padding: 10px; + border-radius: 4px; + } + .nav-link.active { + background-color: #0d6efd; + color: white !important; + } + </style> +</head> +<body> + <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> + <div class="container-fluid"> + <a class="navbar-brand" href="/">MYP API Tester</a> + <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarNav"> + <ul class="navbar-nav"> + <li class="nav-item"> + <a class="nav-link {% if active_page == 'home' %}active{% endif %}" href="/">Home</a> + </li> + <li class="nav-item"> + <a class="nav-link {% if active_page == 'printers' %}active{% endif %}" href="/admin/printers">Drucker</a> + </li> + <li class="nav-item"> + <a class="nav-link {% if active_page == 'jobs' %}active{% endif %}" href="/admin/jobs">Druckaufträge</a> + </li> + <li class="nav-item"> + <a class="nav-link {% if active_page == 'users' %}active{% endif %}" href="/admin/users">Benutzer</a> + </li> + <li class="nav-item"> + <a class="nav-link {% if active_page == 'stats' %}active{% endif %}" href="/admin/stats">Statistiken</a> + </li> + </ul> + <ul class="navbar-nav ms-auto"> + {% if current_user %} + <li class="nav-item dropdown"> + <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown"> + {{ current_user.username }} + </a> + <ul class="dropdown-menu dropdown-menu-end"> + <li><a class="dropdown-item" href="/logout">Abmelden</a></li> + </ul> + </li> + {% else %} + <li class="nav-item"> + <a class="nav-link" href="/login">Anmelden</a> + </li> + {% endif %} + </ul> + </div> + </div> + </nav> + + <div class="container-fluid py-3"> + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + <div class="alert alert-{{ category }}" role="alert"> + {{ message }} + </div> + {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} + </div> + + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> + <script> + function formatJson(jsonString) { + try { + const obj = JSON.parse(jsonString); + return JSON.stringify(obj, null, 2); + } catch (e) { + return jsonString; + } + } + + document.addEventListener('DOMContentLoaded', function() { + // Format all response areas + document.querySelectorAll('.api-response').forEach(function(el) { + if (el.textContent) { + el.textContent = formatJson(el.textContent); + } + }); + + // Add event listener to show response areas + document.querySelectorAll('.api-form').forEach(function(form) { + form.addEventListener('submit', async function(e) { + e.preventDefault(); + + const url = this.getAttribute('data-url'); + const method = this.getAttribute('data-method') || 'GET'; + const responseArea = document.getElementById(this.getAttribute('data-response')); + const formData = new FormData(this); + const data = {}; + + formData.forEach((value, key) => { + if (value) { + try { + // Try to parse as JSON if it looks like JSON + if (value.trim().startsWith('{') || value.trim().startsWith('[')) { + data[key] = JSON.parse(value); + } else { + data[key] = value; + } + } catch (e) { + data[key] = value; + } + } + }); + + const options = { + method: method, + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'same-origin' + }; + + if (method !== 'GET' && method !== 'HEAD') { + options.body = JSON.stringify(data); + } + + try { + responseArea.textContent = 'Sending request...'; + const response = await fetch(url, options); + const responseText = await response.text(); + + try { + const formatted = formatJson(responseText); + responseArea.textContent = formatted; + } catch (e) { + responseArea.textContent = responseText; + } + + if (this.hasAttribute('data-reload') && response.ok) { + setTimeout(() => { + window.location.reload(); + }, 1000); + } + } catch (err) { + responseArea.textContent = 'Error: ' + err.message; + } + }); + }); + }); + </script> + {% block scripts %}{% endblock %} +</body> +</html> \ No newline at end of file diff --git a/backend/templates/dashboard.html b/backend/templates/dashboard.html new file mode 100644 index 0000000..4cfb6ca --- /dev/null +++ b/backend/templates/dashboard.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} + +{% block title %}Dashboard - MYP API Tester{% endblock %} + +{% block content %} +<div class="row"> + <div class="col-md-12 mb-4"> + <div class="card"> + <div class="card-header"> + <h4 class="mb-0">Willkommen, {{ current_user.display_name }}</h4> + </div> + <div class="card-body"> + <p>Benutzerdetails:</p> + <ul> + <li><strong>ID:</strong> {{ current_user.id }}</li> + <li><strong>Benutzername:</strong> {{ current_user.username }}</li> + <li><strong>E-Mail:</strong> {{ current_user.email or "Nicht angegeben" }}</li> + <li><strong>Rolle:</strong> {{ current_user.role }}</li> + </ul> + <div class="mt-3"> + <a href="/admin/printers" class="btn btn-primary me-2">Drucker verwalten</a> + <a href="/admin/jobs" class="btn btn-success me-2">Druckaufträge verwalten</a> + {% if current_user.role == 'admin' %} + <a href="/admin/users" class="btn btn-info me-2">Benutzer verwalten</a> + <a href="/admin/stats" class="btn btn-secondary">Statistiken</a> + {% endif %} + </div> + </div> + </div> + </div> +</div> + +<div class="row"> + <div class="col-md-6 mb-4"> + <div class="card"> + <div class="card-header"> + <h5 class="mb-0">API-Test: GET /api/me</h5> + </div> + <div class="card-body"> + <form class="api-form" data-url="/api/me" data-method="GET" data-response="meResponse"> + <button type="submit" class="btn btn-primary">API aufrufen</button> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="meResponse"></pre> + </div> + </div> + </div> + </div> + + <div class="col-md-6 mb-4"> + <div class="card"> + <div class="card-header"> + <h5 class="mb-0">API-Test: GET /api/test</h5> + </div> + <div class="card-body"> + <form class="api-form" data-url="/api/test" data-method="GET" data-response="testResponse"> + <button type="submit" class="btn btn-primary">API aufrufen</button> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="testResponse"></pre> + </div> + </div> + </div> + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/backend/templates/jobs.html b/backend/templates/jobs.html new file mode 100644 index 0000000..aceea93 --- /dev/null +++ b/backend/templates/jobs.html @@ -0,0 +1,346 @@ +{% extends "base.html" %} + +{% block title %}Druckaufträge - MYP API Tester{% endblock %} + +{% block content %} +<div class="row"> + <div class="col-md-12 mb-4"> + <div class="card"> + <div class="card-header d-flex justify-content-between align-items-center"> + <h4 class="mb-0">Druckaufträge verwalten</h4> + <button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newJobForm"> + Neuen Auftrag erstellen + </button> + </div> + <div class="collapse" id="newJobForm"> + <div class="card-body border-bottom"> + <form class="api-form" data-url="/api/jobs" data-method="POST" data-response="createJobResponse" data-reload="true"> + <div class="mb-3"> + <label for="jobPrinterId" class="form-label">Drucker</label> + <select class="form-control" id="jobPrinterId" name="printerId" required> + <option value="">Drucker auswählen...</option> + <!-- Wird dynamisch gefüllt --> + </select> + </div> + <div class="mb-3"> + <label for="jobDuration" class="form-label">Dauer (Minuten)</label> + <input type="number" class="form-control" id="jobDuration" name="durationInMinutes" min="1" required> + </div> + <div class="mb-3"> + <label for="jobComments" class="form-label">Kommentare</label> + <textarea class="form-control" id="jobComments" name="comments" rows="3"></textarea> + </div> + <button type="submit" class="btn btn-success">Auftrag erstellen</button> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="createJobResponse"></pre> + </div> + </div> + </div> + <div class="card-body"> + <form class="api-form mb-3" data-url="/api/jobs" data-method="GET" data-response="jobsResponse"> + <button type="submit" class="btn btn-primary">Aufträge aktualisieren</button> + </form> + + <div class="table-responsive"> + <table class="table table-striped table-hover"> + <thead> + <tr> + <th>ID</th> + <th>Drucker</th> + <th>Benutzer</th> + <th>Start</th> + <th>Dauer (Min)</th> + <th>Verbleibend (Min)</th> + <th>Status</th> + <th>Kommentare</th> + <th>Aktionen</th> + </tr> + </thead> + <tbody id="jobsTableBody"> + <!-- Wird dynamisch gefüllt --> + </tbody> + </table> + </div> + + <div> + <h6>API-Antwort:</h6> + <pre class="api-response" id="jobsResponse"></pre> + </div> + </div> + </div> + </div> +</div> + +<!-- Job abbrechen Modal --> +<div class="modal fade" id="abortJobModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Auftrag abbrechen</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Möchten Sie den Auftrag wirklich abbrechen?</p> + <form id="abortJobForm" class="api-form" data-method="POST" data-response="abortJobResponse" data-reload="true"> + <input type="hidden" id="abortJobId" name="jobId"> + <div class="mb-3"> + <label for="abortReason" class="form-label">Abbruchgrund</label> + <textarea class="form-control" id="abortReason" name="reason" rows="3"></textarea> + </div> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="abortJobResponse"></pre> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> + <button type="submit" form="abortJobForm" class="btn btn-danger">Auftrag abbrechen</button> + </div> + </div> + </div> +</div> + +<!-- Job beenden Modal --> +<div class="modal fade" id="finishJobModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Auftrag beenden</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Möchten Sie den Auftrag als beendet markieren?</p> + <form id="finishJobForm" class="api-form" data-method="POST" data-response="finishJobResponse" data-reload="true"> + <input type="hidden" id="finishJobId" name="jobId"> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="finishJobResponse"></pre> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> + <button type="submit" form="finishJobForm" class="btn btn-success">Auftrag beenden</button> + </div> + </div> + </div> +</div> + +<!-- Job verlängern Modal --> +<div class="modal fade" id="extendJobModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Auftrag verlängern</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <form id="extendJobForm" class="api-form" data-method="POST" data-response="extendJobResponse" data-reload="true"> + <input type="hidden" id="extendJobId" name="jobId"> + <div class="mb-3"> + <label for="extendHours" class="form-label">Stunden</label> + <input type="number" class="form-control" id="extendHours" name="hours" min="0" value="0"> + </div> + <div class="mb-3"> + <label for="extendMinutes" class="form-label">Minuten</label> + <input type="number" class="form-control" id="extendMinutes" name="minutes" min="0" max="59" value="30"> + </div> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="extendJobResponse"></pre> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> + <button type="submit" form="extendJobForm" class="btn btn-primary">Auftrag verlängern</button> + </div> + </div> + </div> +</div> + +<!-- Job Kommentare bearbeiten Modal --> +<div class="modal fade" id="editCommentsModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Kommentare bearbeiten</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <form id="editCommentsForm" class="api-form" data-method="PUT" data-response="editCommentsResponse" data-reload="true"> + <input type="hidden" id="editCommentsJobId" name="jobId"> + <div class="mb-3"> + <label for="editJobComments" class="form-label">Kommentare</label> + <textarea class="form-control" id="editJobComments" name="comments" rows="3"></textarea> + </div> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="editCommentsResponse"></pre> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> + <button type="submit" form="editCommentsForm" class="btn btn-primary">Speichern</button> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> + document.addEventListener('DOMContentLoaded', function() { + // Drucker für Dropdown laden + loadPrinters(); + + // Aufträge laden + document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit')); + + // Tabelle aktualisieren, wenn Aufträge geladen werden + const jobsResponse = document.getElementById('jobsResponse'); + const observer = new MutationObserver(function(mutations) { + try { + const jobs = JSON.parse(jobsResponse.textContent); + updateJobsTable(jobs); + } catch (e) { + console.error('Fehler beim Parsen der Auftrags-Daten:', e); + } + }); + + observer.observe(jobsResponse, { childList: true, characterData: true, subtree: true }); + + // Abort-Modal vorbereiten + document.getElementById('abortJobModal').addEventListener('show.bs.modal', function(event) { + const button = event.relatedTarget; + const jobId = button.getAttribute('data-job-id'); + + document.getElementById('abortJobId').value = jobId; + document.getElementById('abortJobForm').setAttribute('data-url', `/api/jobs/${jobId}/abort`); + }); + + // Finish-Modal vorbereiten + document.getElementById('finishJobModal').addEventListener('show.bs.modal', function(event) { + const button = event.relatedTarget; + const jobId = button.getAttribute('data-job-id'); + + document.getElementById('finishJobId').value = jobId; + document.getElementById('finishJobForm').setAttribute('data-url', `/api/jobs/${jobId}/finish`); + }); + + // Extend-Modal vorbereiten + document.getElementById('extendJobModal').addEventListener('show.bs.modal', function(event) { + const button = event.relatedTarget; + const jobId = button.getAttribute('data-job-id'); + + document.getElementById('extendJobId').value = jobId; + document.getElementById('extendJobForm').setAttribute('data-url', `/api/jobs/${jobId}/extend`); + }); + + // Edit-Comments-Modal vorbereiten + document.getElementById('editCommentsModal').addEventListener('show.bs.modal', function(event) { + const button = event.relatedTarget; + const jobId = button.getAttribute('data-job-id'); + const comments = button.getAttribute('data-job-comments'); + + document.getElementById('editCommentsJobId').value = jobId; + document.getElementById('editCommentsForm').setAttribute('data-url', `/api/jobs/${jobId}/comments`); + document.getElementById('editJobComments').value = comments || ''; + }); + }); + + async function loadPrinters() { + try { + const response = await fetch('/api/printers'); + const printers = await response.json(); + + const selectElement = document.getElementById('jobPrinterId'); + selectElement.innerHTML = '<option value="">Drucker auswählen...</option>'; + + // Nur verfügbare Drucker anzeigen + printers.filter(printer => printer.status === 0).forEach(printer => { + const option = document.createElement('option'); + option.value = printer.id; + option.textContent = `${printer.name} - ${printer.description}`; + selectElement.appendChild(option); + }); + } catch (e) { + console.error('Fehler beim Laden der Drucker:', e); + } + } + + function updateJobsTable(jobs) { + const tableBody = document.getElementById('jobsTableBody'); + tableBody.innerHTML = ''; + + jobs.forEach(job => { + const row = document.createElement('tr'); + + const startDate = new Date(job.startAt); + const formattedStart = startDate.toLocaleString(); + + const isActive = !job.aborted && job.remainingMinutes > 0; + + let statusText = ''; + let statusClass = ''; + + if (job.aborted) { + statusText = 'Abgebrochen'; + statusClass = 'text-danger'; + } else if (job.remainingMinutes <= 0) { + statusText = 'Abgeschlossen'; + statusClass = 'text-success'; + } else { + statusText = 'Aktiv'; + statusClass = 'text-warning'; + } + + row.innerHTML = ` + <td>${job.id}</td> + <td>${job.printerId}</td> + <td>${job.userId}</td> + <td>${formattedStart}</td> + <td>${job.durationInMinutes}</td> + <td>${job.remainingMinutes}</td> + <td><span class="${statusClass}">${statusText}</span></td> + <td>${job.comments || '-'}</td> + <td> + ${isActive ? ` + <button type="button" class="btn btn-sm btn-danger mb-1" + data-bs-toggle="modal" + data-bs-target="#abortJobModal" + data-job-id="${job.id}"> + Abbrechen + </button> + <button type="button" class="btn btn-sm btn-success mb-1" + data-bs-toggle="modal" + data-bs-target="#finishJobModal" + data-job-id="${job.id}"> + Beenden + </button> + <button type="button" class="btn btn-sm btn-primary mb-1" + data-bs-toggle="modal" + data-bs-target="#extendJobModal" + data-job-id="${job.id}"> + Verlängern + </button> + ` : ''} + <button type="button" class="btn btn-sm btn-secondary mb-1" + data-bs-toggle="modal" + data-bs-target="#editCommentsModal" + data-job-id="${job.id}" + data-job-comments="${job.comments || ''}"> + Kommentare + </button> + </td> + `; + + tableBody.appendChild(row); + }); + } +</script> +{% endblock %} \ No newline at end of file diff --git a/backend/templates/login.html b/backend/templates/login.html new file mode 100644 index 0000000..c8a5cd3 --- /dev/null +++ b/backend/templates/login.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}Anmelden - MYP API Tester{% endblock %} + +{% block content %} +<div class="row justify-content-center"> + <div class="col-md-6"> + <div class="card"> + <div class="card-header"> + <h4 class="mb-0">Anmelden</h4> + </div> + <div class="card-body"> + <form class="api-form" data-url="/auth/login" data-method="POST" data-response="loginResponse"> + <div class="mb-3"> + <label for="username" class="form-label">Benutzername</label> + <input type="text" class="form-control" id="username" name="username" required> + </div> + <div class="mb-3"> + <label for="password" class="form-label">Passwort</label> + <input type="password" class="form-control" id="password" name="password" required> + </div> + <button type="submit" class="btn btn-primary">Anmelden</button> + </form> + + <div class="mt-3"> + <p>Noch kein Konto? <a href="/register">Registrieren</a></p> + </div> + + <div class="mt-3"> + <h5>Antwort:</h5> + <pre class="api-response" id="loginResponse"></pre> + </div> + </div> + </div> + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/backend/templates/printers.html b/backend/templates/printers.html new file mode 100644 index 0000000..773312f --- /dev/null +++ b/backend/templates/printers.html @@ -0,0 +1,248 @@ +{% extends "base.html" %} + +{% block title %}Drucker - MYP API Tester{% endblock %} + +{% block content %} +<div class="row"> + <div class="col-md-12 mb-4"> + <div class="card"> + <div class="card-header d-flex justify-content-between align-items-center"> + <h4 class="mb-0">Drucker verwalten</h4> + <button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newPrinterForm"> + Neuen Drucker hinzufügen + </button> + </div> + <div class="collapse" id="newPrinterForm"> + <div class="card-body border-bottom"> + <form class="api-form" data-url="/api/printers" data-method="POST" data-response="createPrinterResponse" data-reload="true"> + <div class="mb-3"> + <label for="printerName" class="form-label">Name</label> + <input type="text" class="form-control" id="printerName" name="name" required> + </div> + <div class="mb-3"> + <label for="printerDescription" class="form-label">Beschreibung</label> + <textarea class="form-control" id="printerDescription" name="description" rows="3" required></textarea> + </div> + <div class="mb-3"> + <label for="printerStatus" class="form-label">Status</label> + <select class="form-control" id="printerStatus" name="status"> + <option value="0">Verfügbar (0)</option> + <option value="1">Besetzt (1)</option> + <option value="2">Wartung (2)</option> + </select> + </div> + <div class="mb-3"> + <label for="printerIpAddress" class="form-label">IP-Adresse (Tapo Steckdose)</label> + <input type="text" class="form-control" id="printerIpAddress" name="ipAddress" placeholder="z.B. 192.168.1.100"> + </div> + <button type="submit" class="btn btn-success">Drucker erstellen</button> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="createPrinterResponse"></pre> + </div> + </div> + </div> + <div class="card-body"> + <form class="api-form mb-3" data-url="/api/printers" data-method="GET" data-response="printersResponse"> + <button type="submit" class="btn btn-primary">Drucker aktualisieren</button> + </form> + + <div class="table-responsive"> + <table class="table table-striped table-hover"> + <thead> + <tr> + <th>ID</th> + <th>Name</th> + <th>Beschreibung</th> + <th>Status</th> + <th>IP-Adresse</th> + <th>Aktionen</th> + </tr> + </thead> + <tbody id="printersTableBody"> + <!-- Wird dynamisch gefüllt --> + </tbody> + </table> + </div> + + <div> + <h6>API-Antwort:</h6> + <pre class="api-response" id="printersResponse"></pre> + </div> + </div> + </div> + </div> +</div> + +<!-- Drucker bearbeiten Modal --> +<div class="modal fade" id="editPrinterModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Drucker bearbeiten</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <form id="editPrinterForm" class="api-form" data-method="PUT" data-response="editPrinterResponse" data-reload="true"> + <input type="hidden" id="editPrinterId" name="printerId"> + <div class="mb-3"> + <label for="editPrinterName" class="form-label">Name</label> + <input type="text" class="form-control" id="editPrinterName" name="name" required> + </div> + <div class="mb-3"> + <label for="editPrinterDescription" class="form-label">Beschreibung</label> + <textarea class="form-control" id="editPrinterDescription" name="description" rows="3" required></textarea> + </div> + <div class="mb-3"> + <label for="editPrinterStatus" class="form-label">Status</label> + <select class="form-control" id="editPrinterStatus" name="status"> + <option value="0">Verfügbar (0)</option> + <option value="1">Besetzt (1)</option> + <option value="2">Wartung (2)</option> + </select> + </div> + <div class="mb-3"> + <label for="editPrinterIpAddress" class="form-label">IP-Adresse (Tapo Steckdose)</label> + <input type="text" class="form-control" id="editPrinterIpAddress" name="ipAddress" placeholder="z.B. 192.168.1.100"> + </div> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="editPrinterResponse"></pre> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> + <button type="submit" form="editPrinterForm" class="btn btn-primary">Änderungen speichern</button> + </div> + </div> + </div> +</div> + +<!-- Drucker löschen Modal --> +<div class="modal fade" id="deletePrinterModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Drucker löschen</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Möchten Sie den Drucker <span id="deletePrinterName"></span> wirklich löschen?</p> + <form id="deletePrinterForm" class="api-form" data-method="DELETE" data-response="deletePrinterResponse" data-reload="true"> + <input type="hidden" id="deletePrinterId" name="printerId"> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="deletePrinterResponse"></pre> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> + <button type="submit" form="deletePrinterForm" class="btn btn-danger">Löschen</button> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> + document.addEventListener('DOMContentLoaded', function() { + // Drucker laden + document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit')); + + // Tabelle aktualisieren, wenn Drucker geladen werden + const printersResponse = document.getElementById('printersResponse'); + const observer = new MutationObserver(function(mutations) { + try { + const printers = JSON.parse(printersResponse.textContent); + updatePrintersTable(printers); + } catch (e) { + console.error('Fehler beim Parsen der Drucker-Daten:', e); + } + }); + + observer.observe(printersResponse, { childList: true, characterData: true, subtree: true }); + + // Edit-Modal vorbereiten + document.getElementById('editPrinterModal').addEventListener('show.bs.modal', function(event) { + const button = event.relatedTarget; + const printerId = button.getAttribute('data-printer-id'); + const printerName = button.getAttribute('data-printer-name'); + const printerDescription = button.getAttribute('data-printer-description'); + const printerStatus = button.getAttribute('data-printer-status'); + const printerIpAddress = button.getAttribute('data-printer-ip'); + + document.getElementById('editPrinterId').value = printerId; + document.getElementById('editPrinterForm').setAttribute('data-url', `/api/printers/${printerId}`); + document.getElementById('editPrinterName').value = printerName; + document.getElementById('editPrinterDescription').value = printerDescription; + document.getElementById('editPrinterStatus').value = printerStatus; + document.getElementById('editPrinterIpAddress').value = printerIpAddress || ''; + }); + + // Delete-Modal vorbereiten + document.getElementById('deletePrinterModal').addEventListener('show.bs.modal', function(event) { + const button = event.relatedTarget; + const printerId = button.getAttribute('data-printer-id'); + const printerName = button.getAttribute('data-printer-name'); + + document.getElementById('deletePrinterId').value = printerId; + document.getElementById('deletePrinterForm').setAttribute('data-url', `/api/printers/${printerId}`); + document.getElementById('deletePrinterName').textContent = printerName; + }); + }); + + function updatePrintersTable(printers) { + const tableBody = document.getElementById('printersTableBody'); + tableBody.innerHTML = ''; + + printers.forEach(printer => { + const row = document.createElement('tr'); + + const statusText = { + 0: 'Verfügbar', + 1: 'Besetzt', + 2: 'Wartung' + }[printer.status] || 'Unbekannt'; + + const statusClass = { + 0: 'text-success', + 1: 'text-warning', + 2: 'text-danger' + }[printer.status] || ''; + + row.innerHTML = ` + <td>${printer.id}</td> + <td>${printer.name}</td> + <td>${printer.description}</td> + <td><span class="${statusClass}">${statusText} (${printer.status})</span></td> + <td>${printer.ipAddress || '-'}</td> + <td> + <button type="button" class="btn btn-sm btn-primary" + data-bs-toggle="modal" + data-bs-target="#editPrinterModal" + data-printer-id="${printer.id}" + data-printer-name="${printer.name}" + data-printer-description="${printer.description}" + data-printer-status="${printer.status}" + data-printer-ip="${printer.ipAddress || ''}"> + Bearbeiten + </button> + <button type="button" class="btn btn-sm btn-danger" + data-bs-toggle="modal" + data-bs-target="#deletePrinterModal" + data-printer-id="${printer.id}" + data-printer-name="${printer.name}"> + Löschen + </button> + </td> + `; + + tableBody.appendChild(row); + }); + } +</script> +{% endblock %} \ No newline at end of file diff --git a/backend/templates/register.html b/backend/templates/register.html new file mode 100644 index 0000000..1c5c168 --- /dev/null +++ b/backend/templates/register.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block title %}Registrieren - MYP API Tester{% endblock %} + +{% block content %} +<div class="row justify-content-center"> + <div class="col-md-6"> + <div class="card"> + <div class="card-header"> + <h4 class="mb-0">Registrieren</h4> + </div> + <div class="card-body"> + <form class="api-form" data-url="/auth/register" data-method="POST" data-response="registerResponse"> + <div class="mb-3"> + <label for="username" class="form-label">Benutzername</label> + <input type="text" class="form-control" id="username" name="username" required> + </div> + <div class="mb-3"> + <label for="password" class="form-label">Passwort</label> + <input type="password" class="form-control" id="password" name="password" required> + </div> + <div class="mb-3"> + <label for="displayName" class="form-label">Anzeigename</label> + <input type="text" class="form-control" id="displayName" name="displayName"> + </div> + <div class="mb-3"> + <label for="email" class="form-label">E-Mail</label> + <input type="email" class="form-control" id="email" name="email"> + </div> + <button type="submit" class="btn btn-primary">Registrieren</button> + </form> + + <div class="mt-3"> + <p>Bereits registriert? <a href="/login">Anmelden</a></p> + </div> + + <div class="mt-3"> + <h5>Antwort:</h5> + <pre class="api-response" id="registerResponse"></pre> + </div> + </div> + </div> + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/backend/templates/stats.html b/backend/templates/stats.html new file mode 100644 index 0000000..cb06838 --- /dev/null +++ b/backend/templates/stats.html @@ -0,0 +1,137 @@ +{% extends "base.html" %} + +{% block title %}Statistiken - MYP API Tester{% endblock %} + +{% block content %} +<div class="row"> + <div class="col-md-12 mb-4"> + <div class="card"> + <div class="card-header"> + <h4 class="mb-0">Systemstatistiken</h4> + </div> + <div class="card-body"> + <form class="api-form mb-3" data-url="/api/stats" data-method="GET" data-response="statsResponse"> + <button type="submit" class="btn btn-primary">Statistiken aktualisieren</button> + </form> + + <div class="row" id="statsContainer"> + <!-- Wird dynamisch gefüllt --> + </div> + + <div class="mt-4"> + <h6>API-Antwort:</h6> + <pre class="api-response" id="statsResponse"></pre> + </div> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> + document.addEventListener('DOMContentLoaded', function() { + // Statistiken laden + document.querySelector('form[data-url="/api/stats"]').dispatchEvent(new Event('submit')); + + // Statistiken aktualisieren, wenn API-Antwort geladen wird + const statsResponse = document.getElementById('statsResponse'); + const observer = new MutationObserver(function(mutations) { + try { + const stats = JSON.parse(statsResponse.textContent); + updateStatsDisplay(stats); + } catch (e) { + console.error('Fehler beim Parsen der Statistik-Daten:', e); + } + }); + + observer.observe(statsResponse, { childList: true, characterData: true, subtree: true }); + }); + + function updateStatsDisplay(stats) { + const container = document.getElementById('statsContainer'); + container.innerHTML = ''; + + // Drucker-Statistiken + const printerStats = document.createElement('div'); + printerStats.className = 'col-md-4 mb-3'; + printerStats.innerHTML = ` + <div class="card h-100"> + <div class="card-header bg-primary text-white"> + <h5 class="mb-0">Drucker</h5> + </div> + <div class="card-body"> + <div class="d-flex justify-content-between mb-2"> + <span>Gesamt:</span> + <span>${stats.printers.total}</span> + </div> + <div class="d-flex justify-content-between mb-2"> + <span>Verfügbar:</span> + <span>${stats.printers.available}</span> + </div> + <div class="d-flex justify-content-between mb-2"> + <span>Auslastung:</span> + <span>${Math.round(stats.printers.utilization_rate * 100)}%</span> + </div> + <div class="progress mt-3"> + <div class="progress-bar" role="progressbar" + style="width: ${Math.round(stats.printers.utilization_rate * 100)}%"> + ${Math.round(stats.printers.utilization_rate * 100)}% + </div> + </div> + </div> + </div> + `; + + // Job-Statistiken + const jobStats = document.createElement('div'); + jobStats.className = 'col-md-4 mb-3'; + jobStats.innerHTML = ` + <div class="card h-100"> + <div class="card-header bg-success text-white"> + <h5 class="mb-0">Druckaufträge</h5> + </div> + <div class="card-body"> + <div class="d-flex justify-content-between mb-2"> + <span>Gesamt:</span> + <span>${stats.jobs.total}</span> + </div> + <div class="d-flex justify-content-between mb-2"> + <span>Aktiv:</span> + <span>${stats.jobs.active}</span> + </div> + <div class="d-flex justify-content-between mb-2"> + <span>Abgeschlossen:</span> + <span>${stats.jobs.completed}</span> + </div> + <div class="d-flex justify-content-between mb-2"> + <span>Durchschnittliche Dauer:</span> + <span>${stats.jobs.avg_duration} Minuten</span> + </div> + </div> + </div> + `; + + // Benutzer-Statistiken + const userStats = document.createElement('div'); + userStats.className = 'col-md-4 mb-3'; + userStats.innerHTML = ` + <div class="card h-100"> + <div class="card-header bg-info text-white"> + <h5 class="mb-0">Benutzer</h5> + </div> + <div class="card-body"> + <div class="d-flex justify-content-between mb-2"> + <span>Gesamt:</span> + <span>${stats.users.total}</span> + </div> + </div> + </div> + `; + + container.appendChild(printerStats); + container.appendChild(jobStats); + container.appendChild(userStats); + } +</script> +{% endblock %} \ No newline at end of file diff --git a/backend/templates/users.html b/backend/templates/users.html new file mode 100644 index 0000000..7c68e4f --- /dev/null +++ b/backend/templates/users.html @@ -0,0 +1,238 @@ +{% extends "base.html" %} + +{% block title %}Benutzer - MYP API Tester{% endblock %} + +{% block content %} +<div class="row"> + <div class="col-md-12 mb-4"> + <div class="card"> + <div class="card-header d-flex justify-content-between align-items-center"> + <h4 class="mb-0">Benutzer verwalten</h4> + <button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newUserForm"> + Neuen Benutzer hinzufügen + </button> + </div> + <div class="collapse" id="newUserForm"> + <div class="card-body border-bottom"> + <form class="api-form" data-url="/auth/register" data-method="POST" data-response="createUserResponse" data-reload="true"> + <div class="mb-3"> + <label for="userName" class="form-label">Benutzername</label> + <input type="text" class="form-control" id="userName" name="username" required> + </div> + <div class="mb-3"> + <label for="userPassword" class="form-label">Passwort</label> + <input type="password" class="form-control" id="userPassword" name="password" required> + </div> + <div class="mb-3"> + <label for="userDisplayName" class="form-label">Anzeigename</label> + <input type="text" class="form-control" id="userDisplayName" name="displayName"> + </div> + <div class="mb-3"> + <label for="userEmail" class="form-label">E-Mail</label> + <input type="email" class="form-control" id="userEmail" name="email"> + </div> + <button type="submit" class="btn btn-success">Benutzer erstellen</button> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="createUserResponse"></pre> + </div> + </div> + </div> + <div class="card-body"> + <form class="api-form mb-3" data-url="/api/users" data-method="GET" data-response="usersResponse"> + <button type="submit" class="btn btn-primary">Benutzer aktualisieren</button> + </form> + + <div class="table-responsive"> + <table class="table table-striped table-hover"> + <thead> + <tr> + <th>ID</th> + <th>Benutzername</th> + <th>Anzeigename</th> + <th>E-Mail</th> + <th>Rolle</th> + <th>Aktionen</th> + </tr> + </thead> + <tbody id="usersTableBody"> + <!-- Wird dynamisch gefüllt --> + </tbody> + </table> + </div> + + <div> + <h6>API-Antwort:</h6> + <pre class="api-response" id="usersResponse"></pre> + </div> + </div> + </div> + </div> +</div> + +<!-- Benutzer bearbeiten Modal --> +<div class="modal fade" id="editUserModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Benutzer bearbeiten</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <form id="editUserForm" class="api-form" data-method="PUT" data-response="editUserResponse" data-reload="true"> + <input type="hidden" id="editUserId" name="userId"> + <div class="mb-3"> + <label for="editUserName" class="form-label">Benutzername</label> + <input type="text" class="form-control" id="editUserName" name="username" required> + </div> + <div class="mb-3"> + <label for="editUserDisplayName" class="form-label">Anzeigename</label> + <input type="text" class="form-control" id="editUserDisplayName" name="displayName"> + </div> + <div class="mb-3"> + <label for="editUserEmail" class="form-label">E-Mail</label> + <input type="email" class="form-control" id="editUserEmail" name="email"> + </div> + <div class="mb-3"> + <label for="editUserRole" class="form-label">Rolle</label> + <select class="form-control" id="editUserRole" name="role"> + <option value="user">Benutzer</option> + <option value="admin">Administrator</option> + <option value="guest">Gast</option> + </select> + </div> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="editUserResponse"></pre> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> + <button type="submit" form="editUserForm" class="btn btn-primary">Änderungen speichern</button> + </div> + </div> + </div> +</div> + +<!-- Benutzer löschen Modal --> +<div class="modal fade" id="deleteUserModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Benutzer löschen</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Möchten Sie den Benutzer <span id="deleteUserName"></span> wirklich löschen?</p> + <form id="deleteUserForm" class="api-form" data-method="DELETE" data-response="deleteUserResponse" data-reload="true"> + <input type="hidden" id="deleteUserId" name="userId"> + </form> + <div class="mt-3"> + <h6>Antwort:</h6> + <pre class="api-response" id="deleteUserResponse"></pre> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> + <button type="submit" form="deleteUserForm" class="btn btn-danger">Löschen</button> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> + document.addEventListener('DOMContentLoaded', function() { + // Benutzer laden + document.querySelector('form[data-url="/api/users"]').dispatchEvent(new Event('submit')); + + // Tabelle aktualisieren, wenn Benutzer geladen werden + const usersResponse = document.getElementById('usersResponse'); + const observer = new MutationObserver(function(mutations) { + try { + const users = JSON.parse(usersResponse.textContent); + updateUsersTable(users); + } catch (e) { + console.error('Fehler beim Parsen der Benutzer-Daten:', e); + } + }); + + observer.observe(usersResponse, { childList: true, characterData: true, subtree: true }); + + // Edit-Modal vorbereiten + document.getElementById('editUserModal').addEventListener('show.bs.modal', function(event) { + const button = event.relatedTarget; + const userId = button.getAttribute('data-user-id'); + const userName = button.getAttribute('data-user-name'); + const userDisplayName = button.getAttribute('data-user-displayname'); + const userEmail = button.getAttribute('data-user-email'); + const userRole = button.getAttribute('data-user-role'); + + document.getElementById('editUserId').value = userId; + document.getElementById('editUserForm').setAttribute('data-url', `/api/users/${userId}`); + document.getElementById('editUserName').value = userName; + document.getElementById('editUserDisplayName').value = userDisplayName || ''; + document.getElementById('editUserEmail').value = userEmail || ''; + document.getElementById('editUserRole').value = userRole; + }); + + // Delete-Modal vorbereiten + document.getElementById('deleteUserModal').addEventListener('show.bs.modal', function(event) { + const button = event.relatedTarget; + const userId = button.getAttribute('data-user-id'); + const userName = button.getAttribute('data-user-name'); + + document.getElementById('deleteUserId').value = userId; + document.getElementById('deleteUserForm').setAttribute('data-url', `/api/users/${userId}`); + document.getElementById('deleteUserName').textContent = userName; + }); + }); + + function updateUsersTable(users) { + const tableBody = document.getElementById('usersTableBody'); + tableBody.innerHTML = ''; + + users.forEach(user => { + const row = document.createElement('tr'); + + const roleClass = { + 'admin': 'text-danger', + 'user': 'text-primary', + 'guest': 'text-secondary' + }[user.role] || ''; + + row.innerHTML = ` + <td>${user.id}</td> + <td>${user.username}</td> + <td>${user.displayName || user.username}</td> + <td>${user.email || '-'}</td> + <td><span class="${roleClass}">${user.role}</span></td> + <td> + <button type="button" class="btn btn-sm btn-primary" + data-bs-toggle="modal" + data-bs-target="#editUserModal" + data-user-id="${user.id}" + data-user-name="${user.username}" + data-user-displayname="${user.displayName || ''}" + data-user-email="${user.email || ''}" + data-user-role="${user.role}"> + Bearbeiten + </button> + <button type="button" class="btn btn-sm btn-danger" + data-bs-toggle="modal" + data-bs-target="#deleteUserModal" + data-user-id="${user.id}" + data-user-name="${user.username}"> + Löschen + </button> + </td> + `; + + tableBody.appendChild(row); + }); + } +</script> +{% endblock %} \ No newline at end of file