1465 lines
65 KiB
Python
1465 lines
65 KiB
Python
import json
|
|
import secrets
|
|
import bcrypt
|
|
from datetime import datetime, timedelta
|
|
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort, session, flash
|
|
from flask_login import current_user, login_required
|
|
from flask_wtf import FlaskForm
|
|
from flask_wtf.file import FileField, FileAllowed
|
|
from wtforms import StringField, TextAreaField, IntegerField, SelectField
|
|
from wtforms.validators import DataRequired, Email, Optional, NumberRange
|
|
from functools import wraps
|
|
from sqlalchemy import desc
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
from models import GuestRequest, Job, Printer, User, UserPermission, Notification, get_cached_session
|
|
from utils.logging_config import get_logger
|
|
from utils.hardware_integration import get_drucker_steuerung
|
|
|
|
guest_blueprint = Blueprint('guest', __name__)
|
|
logger = get_logger("guest")
|
|
|
|
# Flask-WTF Formular für Gastanfragen
|
|
class GuestRequestForm(FlaskForm):
|
|
name = StringField('Vollständiger Name', validators=[DataRequired()])
|
|
email = StringField('E-Mail-Adresse', validators=[DataRequired(), Email()])
|
|
printer_id = SelectField('Drucker auswählen', coerce=int, validators=[Optional()])
|
|
duration_min = IntegerField('Geschätzte Dauer (Minuten)',
|
|
validators=[DataRequired(), NumberRange(min=1, max=1440)],
|
|
default=60)
|
|
reason = TextAreaField('Projektbeschreibung', validators=[Optional()])
|
|
file = FileField('3D-Datei hochladen',
|
|
validators=[Optional(), FileAllowed(['stl', 'obj', '3mf', 'amf', 'gcode'],
|
|
'3D-Dateien sind erlaubt!')])
|
|
|
|
# Hilfsfunktionen
|
|
# Importiere Berechtigungsfunktionen aus utils.permissions
|
|
from utils.permissions import can_approve_jobs, approver_required
|
|
|
|
# Gast-Routen
|
|
@guest_blueprint.route('/request', methods=['GET', 'POST'])
|
|
def guest_request_form():
|
|
"""Formular für Gastanfragen anzeigen und verarbeiten."""
|
|
with get_cached_session() as db_session:
|
|
# Nur Drucker von TBA Marienfelde für Auswahlfelder anzeigen
|
|
printers = db_session.query(Printer).filter(
|
|
Printer.location == "TBA Marienfelde"
|
|
).all()
|
|
|
|
# Formular erstellen
|
|
form = GuestRequestForm()
|
|
|
|
# Drucker-Optionen für SelectField setzen
|
|
printer_choices = [(0, 'Keinen spezifischen Drucker auswählen')]
|
|
printer_choices.extend([(p.id, f"{p.name} ({p.location or 'Kein Standort'})") for p in printers])
|
|
form.printer_id.choices = printer_choices
|
|
|
|
if form.validate_on_submit():
|
|
try:
|
|
# Daten aus dem Formular extrahieren
|
|
name = form.name.data
|
|
email = form.email.data
|
|
reason = form.reason.data
|
|
duration_min = form.duration_min.data
|
|
printer_id = form.printer_id.data if form.printer_id.data != 0 else None
|
|
|
|
# IP-Adresse erfassen
|
|
author_ip = request.remote_addr
|
|
|
|
# Drucker validieren, falls angegeben
|
|
if printer_id:
|
|
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
|
|
if not printer:
|
|
flash("Ungültiger Drucker ausgewählt.", "error")
|
|
return render_template('guest_request.html', form=form, printers=printers)
|
|
|
|
# Neue Anfrage erstellen
|
|
guest_request = GuestRequest(
|
|
name=name,
|
|
email=email,
|
|
reason=reason,
|
|
duration_min=duration_min,
|
|
printer_id=printer_id,
|
|
author_ip=author_ip
|
|
)
|
|
|
|
db_session.add(guest_request)
|
|
db_session.flush() # Um ID zu erhalten
|
|
|
|
# OTP-Code sofort generieren für Status-Abfrage
|
|
otp_code = guest_request.generate_otp()
|
|
guest_request.otp_expires_at = datetime.now() + timedelta(hours=72) # 72h gültig
|
|
|
|
db_session.commit()
|
|
|
|
# Benachrichtigung für Genehmiger erstellen
|
|
Notification.create_for_approvers(
|
|
notification_type="guest_request",
|
|
payload={
|
|
"request_id": guest_request.id,
|
|
"name": guest_request.name,
|
|
"email": guest_request.email,
|
|
"reason": guest_request.reason,
|
|
"duration_min": guest_request.duration_min,
|
|
"printer_name": printer.name if printer_id and printer else "Kein spezifischer Drucker",
|
|
"created_at": guest_request.created_at.isoformat(),
|
|
"status": guest_request.status,
|
|
"author_ip": author_ip
|
|
}
|
|
)
|
|
|
|
logger.info(f"Neue Gastanfrage erstellt: ID {guest_request.id}, Name: {name}, OTP generiert")
|
|
flash("Ihr Antrag wurde erfolgreich eingereicht!", "success")
|
|
|
|
# Weiterleitung zur Status-Seite mit OTP-Code-Info
|
|
return redirect(url_for('guest.guest_request_status', request_id=guest_request.id))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Erstellen der Gastanfrage: {str(e)}")
|
|
flash("Fehler beim Verarbeiten Ihres Antrags. Bitte versuchen Sie es erneut.", "error")
|
|
|
|
# Drucker-Liste von der Session trennen für Template-Verwendung
|
|
db_session.expunge_all()
|
|
|
|
return render_template('guest_request.html', form=form, printers=printers)
|
|
|
|
@guest_blueprint.route('/start', methods=['GET'])
|
|
def guest_start_public():
|
|
"""Öffentliche Code-Eingabe-Seite für Gäste (ohne Anmeldung)."""
|
|
return render_template('guest_start_job.html')
|
|
|
|
@guest_blueprint.route('/job/<int:job_id>/status', methods=['GET'])
|
|
def guest_job_status(job_id):
|
|
"""Job-Status-Seite für Gäste anzeigen."""
|
|
with get_cached_session() as db_session:
|
|
# Job mit eager loading des printer-Relationships laden
|
|
job = db_session.query(Job).options(
|
|
joinedload(Job.printer),
|
|
joinedload(Job.user)
|
|
).filter_by(id=job_id).first()
|
|
|
|
if not job:
|
|
abort(404, "Job nicht gefunden")
|
|
|
|
# Zugehörige Gastanfrage finden
|
|
guest_request = db_session.query(GuestRequest).filter_by(job_id=job_id).first()
|
|
|
|
# Objekte explizit von der Session trennen, um sie außerhalb verwenden zu können
|
|
db_session.expunge(job)
|
|
if guest_request:
|
|
db_session.expunge(guest_request)
|
|
|
|
return render_template('guest_job_status.html',
|
|
job=job,
|
|
guest_request=guest_request)
|
|
|
|
@guest_blueprint.route('/requests/overview', methods=['GET'])
|
|
def guest_requests_overview():
|
|
"""Öffentliche Übersicht aller Druckanträge mit zensierten persönlichen Daten."""
|
|
try:
|
|
with get_cached_session() as db_session:
|
|
# Alle Gastanfragen mit eager loading des printer-Relationships laden
|
|
guest_requests = db_session.query(GuestRequest).options(
|
|
joinedload(GuestRequest.printer)
|
|
).order_by(desc(GuestRequest.created_at)).all()
|
|
|
|
# Daten für Gäste aufbereiten (persönliche Daten zensieren)
|
|
public_requests = []
|
|
for req in guest_requests:
|
|
# Name zensieren: Nur ersten Buchstaben und letzten Buchstaben anzeigen
|
|
censored_name = "***"
|
|
if req.name and len(req.name) > 0:
|
|
if len(req.name) == 1:
|
|
censored_name = req.name[0] + "***"
|
|
elif len(req.name) == 2:
|
|
censored_name = req.name[0] + "***" + req.name[-1]
|
|
else:
|
|
censored_name = req.name[0] + "***" + req.name[-1]
|
|
|
|
# E-Mail zensieren
|
|
censored_email = "***@***.***"
|
|
if req.email and "@" in req.email:
|
|
email_parts = req.email.split("@")
|
|
if len(email_parts[0]) > 2:
|
|
censored_email = email_parts[0][:2] + "***@" + email_parts[1]
|
|
else:
|
|
censored_email = "***@" + email_parts[1]
|
|
|
|
# Grund zensieren (nur erste 20 Zeichen anzeigen)
|
|
censored_reason = "***"
|
|
if req.reason and len(req.reason) > 20:
|
|
censored_reason = req.reason[:20] + "..."
|
|
elif req.reason:
|
|
censored_reason = req.reason
|
|
|
|
public_requests.append({
|
|
"id": req.id,
|
|
"name": censored_name,
|
|
"email": censored_email,
|
|
"reason": censored_reason,
|
|
"duration_min": req.duration_min,
|
|
"created_at": req.created_at,
|
|
"status": req.status,
|
|
"printer": req.printer.to_dict() if req.printer else None
|
|
})
|
|
|
|
# Objekte explizit von der Session trennen
|
|
db_session.expunge_all()
|
|
|
|
return render_template('guest_requests_overview.html', requests=public_requests)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden der öffentlichen Gastanfragen: {str(e)}")
|
|
return render_template('guest_requests_overview.html', requests=[], error="Fehler beim Laden der Anfragen")
|
|
|
|
@guest_blueprint.route('/request/<int:request_id>', methods=['GET'])
|
|
def guest_request_status(request_id):
|
|
"""Status einer Gastanfrage anzeigen."""
|
|
with get_cached_session() as db_session:
|
|
# Guest Request mit eager loading des printer-Relationships laden
|
|
guest_request = db_session.query(GuestRequest).options(
|
|
joinedload(GuestRequest.printer)
|
|
).filter_by(id=request_id).first()
|
|
|
|
if not guest_request:
|
|
abort(404, "Anfrage nicht gefunden")
|
|
|
|
# OTP-Code nur anzeigen, wenn Anfrage genehmigt wurde
|
|
otp_code = None
|
|
show_start_link = False
|
|
|
|
if guest_request.status == "approved":
|
|
if not guest_request.otp_code:
|
|
# OTP generieren falls noch nicht vorhanden
|
|
otp_code = guest_request.generate_otp()
|
|
db_session.commit()
|
|
else:
|
|
# OTP existiert bereits - prüfen ob noch nicht verwendet
|
|
show_start_link = guest_request.otp_used_at is None
|
|
|
|
# Zugehörigen Job laden, falls vorhanden
|
|
job = None
|
|
if guest_request.job_id:
|
|
job = db_session.query(Job).filter_by(id=guest_request.job_id).first()
|
|
|
|
# Objekte explizit von der Session trennen, um sie außerhalb verwenden zu können
|
|
db_session.expunge(guest_request)
|
|
if job:
|
|
db_session.expunge(job)
|
|
|
|
return render_template('guest_status.html',
|
|
request=guest_request,
|
|
job=job,
|
|
otp_code=otp_code,
|
|
show_start_link=show_start_link)
|
|
|
|
@guest_blueprint.route('/guest/requests', methods=['GET'])
|
|
def guest_requests_by_email():
|
|
"""Guest-Requests für eine bestimmte E-Mail-Adresse anzeigen."""
|
|
email = request.args.get('email')
|
|
|
|
if not email:
|
|
# Ohne E-Mail-Parameter zur allgemeinen Übersicht weiterleiten
|
|
return redirect(url_for('guest.guest_requests_overview'))
|
|
|
|
try:
|
|
with get_cached_session() as db_session:
|
|
# Guest-Requests für die angegebene E-Mail-Adresse laden
|
|
guest_requests = db_session.query(GuestRequest).options(
|
|
joinedload(GuestRequest.printer)
|
|
).filter_by(email=email).order_by(desc(GuestRequest.created_at)).all()
|
|
|
|
# Jobs für die Requests laden falls vorhanden
|
|
request_data = []
|
|
for req in guest_requests:
|
|
job = None
|
|
if req.job_id:
|
|
job = db_session.query(Job).options(
|
|
joinedload(Job.printer)
|
|
).filter_by(id=req.job_id).first()
|
|
|
|
request_data.append({
|
|
'request': req,
|
|
'job': job
|
|
})
|
|
|
|
# Objekte von der Session trennen
|
|
db_session.expunge_all()
|
|
|
|
return render_template('guest_requests_by_email.html',
|
|
requests=request_data,
|
|
email=email)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden der Guest-Requests für E-Mail {email}: {str(e)}")
|
|
return render_template('guest_requests_by_email.html',
|
|
requests=[],
|
|
email=email,
|
|
error="Fehler beim Laden der Anfragen")
|
|
|
|
# API-Endpunkte
|
|
@guest_blueprint.route('/api/guest/requests', methods=['POST'])
|
|
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
|
|
def api_create_guest_request():
|
|
"""Neue Gastanfrage erstellen."""
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({"error": "Keine Daten erhalten"}), 400
|
|
|
|
# Pflichtfelder prüfen
|
|
name = data.get('name')
|
|
if not name:
|
|
return jsonify({"error": "Name ist erforderlich"}), 400
|
|
|
|
# Optionale Felder
|
|
email = data.get('email')
|
|
reason = data.get('reason')
|
|
duration_min = data.get('duration_min', 60) # Standard: 1 Stunde
|
|
printer_id = data.get('printer_id')
|
|
|
|
# IP-Adresse erfassen
|
|
author_ip = request.remote_addr
|
|
|
|
try:
|
|
with get_cached_session() as db_session:
|
|
# Drucker prüfen
|
|
if printer_id:
|
|
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
|
|
if not printer:
|
|
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
|
|
|
|
# Neue Anfrage erstellen
|
|
guest_request = GuestRequest(
|
|
name=name,
|
|
email=email,
|
|
reason=reason,
|
|
duration_min=duration_min,
|
|
printer_id=printer_id,
|
|
author_ip=author_ip
|
|
)
|
|
|
|
db_session.add(guest_request)
|
|
db_session.flush() # Um ID zu erhalten
|
|
|
|
# OTP-Code sofort generieren für Status-Abfrage
|
|
otp_code = guest_request.generate_otp()
|
|
guest_request.otp_expires_at = datetime.now() + timedelta(hours=72) # 72h gültig
|
|
|
|
db_session.commit()
|
|
|
|
# Benachrichtigung für Genehmiger erstellen
|
|
Notification.create_for_approvers(
|
|
notification_type="guest_request",
|
|
payload={
|
|
"request_id": guest_request.id,
|
|
"name": guest_request.name,
|
|
"email": guest_request.email,
|
|
"reason": guest_request.reason,
|
|
"duration_min": guest_request.duration_min,
|
|
"printer_name": printer.name if printer_id and printer else "Kein spezifischer Drucker",
|
|
"created_at": guest_request.created_at.isoformat(),
|
|
"status": guest_request.status,
|
|
"author_ip": author_ip
|
|
}
|
|
)
|
|
|
|
logger.info(f"Neue Gastanfrage erstellt: ID {guest_request.id}, Name: {name}, OTP generiert")
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"request_id": guest_request.id,
|
|
"status": guest_request.status,
|
|
"otp_code": otp_code, # Code wird nur bei Erstellung zurückgegeben
|
|
"status_check_url": url_for('guest.guest_status_check_page', _external=True),
|
|
"redirect_url": url_for('guest.guest_request_status', request_id=guest_request.id)
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Erstellen der Gastanfrage: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
|
|
|
@guest_blueprint.route('/api/guest/start-job', methods=['POST'])
|
|
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
|
|
def api_start_job_with_code():
|
|
"""Job mit 6-stelligem OTP-Code starten (vereinfacht - nur Code erforderlich)."""
|
|
try:
|
|
data = request.get_json()
|
|
if not data or 'code' not in data:
|
|
return jsonify({"error": "Code ist erforderlich"}), 400
|
|
|
|
code = data['code'].strip().upper()
|
|
|
|
if len(code) != 6:
|
|
return jsonify({"error": "Code muss 6 Zeichen lang sein"}), 400
|
|
|
|
with get_cached_session() as db_session:
|
|
# Gastanfrage nur anhand des OTP-Codes finden (vereinfacht)
|
|
matching_request = GuestRequest.find_by_otp(code)
|
|
|
|
if not matching_request:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": "Ungültiger Code oder Code bereits verwendet"
|
|
}), 400
|
|
|
|
# Prüfen ob zugehöriger Job existiert
|
|
if not matching_request.job_id:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": "Kein zugehöriger Job gefunden"
|
|
}), 400
|
|
|
|
job = db_session.query(Job).options(
|
|
joinedload(Job.printer)
|
|
).filter_by(id=matching_request.job_id).first()
|
|
|
|
if not job:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": "Job nicht gefunden"
|
|
}), 400
|
|
|
|
# Prüfen ob Job noch startbar ist
|
|
if job.status not in ["scheduled", "waiting_for_printer"]:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": f"Job kann im Status '{job.status}' nicht gestartet werden"
|
|
}), 400
|
|
|
|
# ===== NEUE DRUCKER-VERFÜGBARKEITSPRÜFUNG =====
|
|
drucker_steuerung = get_drucker_steuerung()
|
|
|
|
# 1. Prüfen ob ein spezifischer Drucker dem Job zugewiesen ist
|
|
if job.printer and job.printer.plug_ip:
|
|
# Spezifischen Drucker-Status prüfen
|
|
try:
|
|
reachable, power_status = drucker_steuerung.check_outlet_status(
|
|
job.printer.plug_ip, job.printer.id
|
|
)
|
|
|
|
if not reachable:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": f"Drucker '{job.printer.name}' ist nicht erreichbar und kann nicht gestartet werden",
|
|
"error_details": {
|
|
"type": "printer_offline",
|
|
"printer_name": job.printer.name,
|
|
"printer_id": job.printer.id,
|
|
"reason": "Drucker ist im Netzwerk nicht erreichbar"
|
|
}
|
|
}), 400
|
|
|
|
# Drucker ist erreichbar - kann gestartet werden
|
|
logger.info(f"✅ Drucker-Verfügbarkeitsprüfung bestanden: {job.printer.name} (Status: {power_status})")
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Fehler bei Drucker-Status-Prüfung für {job.printer.name}: {e}")
|
|
return jsonify({
|
|
"success": False,
|
|
"error": f"Drucker-Status für '{job.printer.name}' konnte nicht geprüft werden",
|
|
"error_details": {
|
|
"type": "printer_check_failed",
|
|
"printer_name": job.printer.name,
|
|
"printer_id": job.printer.id,
|
|
"reason": "Technischer Fehler bei der Status-Prüfung"
|
|
}
|
|
}), 500
|
|
|
|
else:
|
|
# 2. Kein spezifischer Drucker zugewiesen - prüfen ob IRGENDEIN Drucker verfügbar ist
|
|
logger.info("🔍 Kein spezifischer Drucker zugewiesen - prüfe alle verfügbaren Drucker")
|
|
|
|
# Alle aktiven Drucker mit Steckdosen laden
|
|
available_printers = db_session.query(Printer).filter(
|
|
Printer.active == True,
|
|
Printer.plug_ip.isnot(None)
|
|
).all()
|
|
|
|
if not available_printers:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": "Keine konfigurierten Drucker verfügbar",
|
|
"error_details": {
|
|
"type": "no_printers_configured",
|
|
"reason": "Es sind keine aktiven Drucker mit Steckdosen-Steuerung konfiguriert"
|
|
}
|
|
}), 400
|
|
|
|
# Status aller verfügbaren Drucker prüfen
|
|
reachable_printers = []
|
|
offline_printers = []
|
|
|
|
for printer in available_printers:
|
|
try:
|
|
reachable, power_status = drucker_steuerung.check_outlet_status(
|
|
printer.plug_ip, printer.id
|
|
)
|
|
|
|
if reachable:
|
|
reachable_printers.append({
|
|
'id': printer.id,
|
|
'name': printer.name,
|
|
'status': power_status
|
|
})
|
|
else:
|
|
offline_printers.append({
|
|
'id': printer.id,
|
|
'name': printer.name
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Status-Prüfung für Drucker {printer.name} fehlgeschlagen: {e}")
|
|
offline_printers.append({
|
|
'id': printer.id,
|
|
'name': printer.name,
|
|
'error': str(e)
|
|
})
|
|
|
|
# Prüfen ob mindestens ein Drucker erreichbar ist
|
|
if not reachable_printers:
|
|
total_printers = len(available_printers)
|
|
offline_count = len(offline_printers)
|
|
|
|
offline_names = [p['name'] for p in offline_printers[:3]] # Max 3 Namen
|
|
offline_list = ', '.join(offline_names)
|
|
if offline_count > 3:
|
|
offline_list += f" und {offline_count - 3} weitere"
|
|
|
|
return jsonify({
|
|
"success": False,
|
|
"error": f"Alle {total_printers} Drucker sind offline und können nicht gestartet werden",
|
|
"error_details": {
|
|
"type": "all_printers_offline",
|
|
"total_printers": total_printers,
|
|
"offline_printers": offline_count,
|
|
"offline_list": offline_list,
|
|
"reason": "Keine Drucker sind derzeit erreichbar oder einsatzbereit"
|
|
}
|
|
}), 400
|
|
|
|
# Mindestens ein Drucker ist verfügbar
|
|
reachable_count = len(reachable_printers)
|
|
total_count = len(available_printers)
|
|
|
|
logger.info(f"✅ Drucker-Verfügbarkeitsprüfung bestanden: {reachable_count}/{total_count} Drucker erreichbar")
|
|
|
|
# Optional: Ersten verfügbaren Drucker automatisch zuweisen wenn keiner zugewiesen
|
|
if not job.printer_id and reachable_printers:
|
|
best_printer = reachable_printers[0] # Ersten verfügbaren nehmen
|
|
job.printer_id = best_printer['id']
|
|
logger.info(f"🎯 Drucker automatisch zugewiesen: {best_printer['name']} (ID: {best_printer['id']})")
|
|
|
|
# ===== JOB STARTEN (bestehender Code) =====
|
|
|
|
# Job starten
|
|
now = datetime.now()
|
|
job.status = "running"
|
|
job.start_at = now
|
|
job.end_at = now + timedelta(minutes=matching_request.duration_min or matching_request.duration_minutes or 60)
|
|
job.actual_start_time = now
|
|
|
|
# OTP als verwendet markieren
|
|
matching_request.mark_otp_used()
|
|
|
|
# Drucker einschalten über Tapo-Steckdose
|
|
if job.printer and job.printer.plug_ip:
|
|
try:
|
|
from utils.job_scheduler import BackgroundTaskScheduler
|
|
scheduler = BackgroundTaskScheduler()
|
|
plug_success = scheduler.toggle_printer_plug(job.printer_id, True)
|
|
if plug_success:
|
|
logger.info(f"🔌 Drucker für Gast-Job {job.id} eingeschaltet")
|
|
else:
|
|
logger.warning(f"⚠️ Steckdose für Gast-Job {job.id} konnte nicht eingeschaltet werden")
|
|
except Exception as e:
|
|
logger.warning(f"Fehler beim Einschalten des Druckers: {str(e)}")
|
|
|
|
# Response-Daten vor Session-Commit sammeln
|
|
response_data = {
|
|
"success": True,
|
|
"job_id": job.id,
|
|
"job_name": job.name,
|
|
"start_time": job.start_at.strftime("%H:%M"),
|
|
"end_time": job.end_at.strftime("%H:%M"),
|
|
"duration_minutes": matching_request.duration_min or matching_request.duration_minutes or 60,
|
|
"printer_name": job.printer.name if job.printer else "Unbekannt",
|
|
"message": f"Job '{job.name}' erfolgreich gestartet"
|
|
}
|
|
|
|
db_session.commit()
|
|
|
|
logger.info(f"Job {job.id} mit 6-stelligem OTP-Code gestartet für Gastanfrage {matching_request.id}")
|
|
|
|
return jsonify(response_data)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Starten des Jobs mit Code: {str(e)}")
|
|
return jsonify({
|
|
"success": False,
|
|
"error": "Fehler beim Starten des Jobs"
|
|
}), 500
|
|
|
|
@guest_blueprint.route('/api/guest/requests/<int:request_id>', methods=['GET'])
|
|
def api_get_guest_request(request_id):
|
|
"""Status einer Gastanfrage abrufen."""
|
|
try:
|
|
with get_cached_session() as db_session:
|
|
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
|
if not guest_request:
|
|
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
|
|
|
# OTP wird nie über die API zurückgegeben
|
|
response_data = guest_request.to_dict()
|
|
response_data.pop("otp_code", None)
|
|
|
|
return jsonify(response_data)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen der Gastanfrage: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
|
|
|
@guest_blueprint.route('/api/guest/job/<int:job_id>/status', methods=['GET'])
|
|
def api_get_guest_job_status(job_id):
|
|
"""Job-Status für Gäste abrufen."""
|
|
try:
|
|
with get_cached_session() as db_session:
|
|
# Job mit Drucker-Information laden
|
|
job = db_session.query(Job).options(
|
|
joinedload(Job.printer)
|
|
).filter_by(id=job_id).first()
|
|
|
|
if not job:
|
|
return jsonify({"error": "Job nicht gefunden"}), 404
|
|
|
|
# Zugehörige Gastanfrage prüfen
|
|
guest_request = db_session.query(GuestRequest).filter_by(job_id=job_id).first()
|
|
if not guest_request:
|
|
return jsonify({"error": "Kein Gastjob"}), 403
|
|
|
|
# Aktuelle Zeit für Berechnungen
|
|
now = datetime.now()
|
|
|
|
# Restzeit berechnen
|
|
remaining_minutes = 0
|
|
if job.status == "running" and job.end_at:
|
|
remaining_seconds = (job.end_at - now).total_seconds()
|
|
remaining_minutes = max(0, int(remaining_seconds / 60))
|
|
|
|
# Fortschritt berechnen
|
|
progress_percent = 0
|
|
if job.status == "running" and job.start_at and job.end_at:
|
|
total_duration = (job.end_at - job.start_at).total_seconds()
|
|
elapsed_duration = (now - job.start_at).total_seconds()
|
|
progress_percent = min(100, max(0, int((elapsed_duration / total_duration) * 100)))
|
|
elif job.status in ["completed", "finished"]:
|
|
progress_percent = 100
|
|
|
|
job_data = {
|
|
"id": job.id,
|
|
"name": job.name,
|
|
"status": job.status,
|
|
"start_at": job.start_at.isoformat() if job.start_at else None,
|
|
"end_at": job.end_at.isoformat() if job.end_at else None,
|
|
"duration_minutes": job.duration_minutes,
|
|
"remaining_minutes": remaining_minutes,
|
|
"progress_percent": progress_percent,
|
|
"printer": {
|
|
"id": job.printer.id,
|
|
"name": job.printer.name,
|
|
"location": job.printer.location
|
|
} if job.printer else None,
|
|
"guest_request": {
|
|
"id": guest_request.id,
|
|
"name": guest_request.name,
|
|
"created_at": guest_request.created_at.isoformat()
|
|
},
|
|
"is_active": job.status in ["scheduled", "running"],
|
|
"is_completed": job.status in ["completed", "finished"],
|
|
"is_failed": job.status in ["failed", "cancelled"]
|
|
}
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"job": job_data
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen des Job-Status: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
|
|
|
@guest_blueprint.route('/api/notifications', methods=['GET'])
|
|
@login_required
|
|
def api_get_notifications():
|
|
"""Benachrichtigungen für den aktuellen Benutzer abrufen."""
|
|
try:
|
|
# Zeitstempel für Filter (nur neue Benachrichtigungen)
|
|
since = request.args.get('since')
|
|
if since:
|
|
try:
|
|
since_date = datetime.fromisoformat(since)
|
|
except ValueError:
|
|
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
|
else:
|
|
since_date = None
|
|
|
|
with get_cached_session() as db_session:
|
|
query = db_session.query(Notification).filter_by(
|
|
user_id=current_user.id,
|
|
is_read=False
|
|
)
|
|
|
|
if since_date:
|
|
query = query.filter(Notification.created_at > since_date)
|
|
|
|
notifications = query.order_by(desc(Notification.created_at)).all()
|
|
|
|
return jsonify({
|
|
"count": len(notifications),
|
|
"notifications": [n.to_dict() for n in notifications]
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen der Benachrichtigungen: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
|
|
|
@guest_blueprint.route('/api/notifications/<int:notification_id>/read', methods=['POST'])
|
|
@login_required
|
|
def api_mark_notification_read(notification_id):
|
|
"""Benachrichtigung als gelesen markieren."""
|
|
try:
|
|
with get_cached_session() as db_session:
|
|
notification = db_session.query(Notification).filter_by(
|
|
id=notification_id,
|
|
user_id=current_user.id
|
|
).first()
|
|
|
|
if not notification:
|
|
return jsonify({"error": "Benachrichtigung nicht gefunden"}), 404
|
|
|
|
notification.is_read = True
|
|
db_session.commit()
|
|
|
|
return jsonify({"success": True})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Markieren der Benachrichtigung als gelesen: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
|
|
|
@guest_blueprint.route('/api/admin/requests', methods=['GET'])
|
|
@approver_required
|
|
def api_get_all_requests():
|
|
"""Alle Gastanfragen für Admins abrufen."""
|
|
try:
|
|
# Filter-Parameter
|
|
status_filter = request.args.get('status', 'all') # all, pending, approved, denied
|
|
limit = int(request.args.get('limit', 50))
|
|
offset = int(request.args.get('offset', 0))
|
|
|
|
with get_cached_session() as db_session:
|
|
# Query mit eager loading
|
|
query = db_session.query(GuestRequest).options(
|
|
joinedload(GuestRequest.printer),
|
|
joinedload(GuestRequest.job),
|
|
joinedload(GuestRequest.processed_by_user)
|
|
)
|
|
|
|
# Status-Filter anwenden
|
|
if status_filter != 'all':
|
|
query = query.filter(GuestRequest.status == status_filter)
|
|
|
|
# Sortierung: Pending zuerst, dann nach Erstellungsdatum
|
|
query = query.order_by(
|
|
desc(GuestRequest.status == 'pending'),
|
|
desc(GuestRequest.created_at)
|
|
)
|
|
|
|
# Pagination
|
|
total_count = query.count()
|
|
requests = query.offset(offset).limit(limit).all()
|
|
|
|
# Daten für Admin aufbereiten
|
|
admin_requests = []
|
|
for req in requests:
|
|
request_data = req.to_dict()
|
|
|
|
# Zusätzliche Admin-Informationen
|
|
request_data.update({
|
|
"can_be_processed": req.status == "pending",
|
|
"is_overdue": (
|
|
req.status == "approved" and
|
|
req.job and
|
|
req.job.end_at < datetime.now()
|
|
) if req.job else False,
|
|
"time_since_creation": (datetime.now() - req.created_at).total_seconds() / 3600 if req.created_at else 0 # Stunden
|
|
})
|
|
|
|
admin_requests.append(request_data)
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"requests": admin_requests,
|
|
"pagination": {
|
|
"total": total_count,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"has_more": (offset + limit) < total_count
|
|
},
|
|
"stats": {
|
|
"total": total_count,
|
|
"pending": db_session.query(GuestRequest).filter_by(status='pending').count(),
|
|
"approved": db_session.query(GuestRequest).filter_by(status='approved').count(),
|
|
"denied": db_session.query(GuestRequest).filter_by(status='denied').count()
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen der Admin-Gastanfragen: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
|
|
|
@guest_blueprint.route('/api/admin/requests/<int:request_id>', methods=['GET'])
|
|
@approver_required
|
|
def api_get_request_details(request_id):
|
|
"""Detaillierte Informationen zu einer Gastanfrage für Admins abrufen."""
|
|
try:
|
|
with get_cached_session() as db_session:
|
|
guest_request = db_session.query(GuestRequest).options(
|
|
joinedload(GuestRequest.printer),
|
|
joinedload(GuestRequest.job),
|
|
joinedload(GuestRequest.processed_by_user)
|
|
).filter_by(id=request_id).first()
|
|
|
|
if not guest_request:
|
|
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
|
|
|
# Vollständige Admin-Informationen
|
|
request_data = guest_request.to_dict()
|
|
|
|
# Verfügbare Drucker für Zuweisung
|
|
# Alle Drucker für Auswahlfelder anzeigen (unabhängig von active-Status)
|
|
available_printers = db_session.query(Printer).all()
|
|
request_data["available_printers"] = [p.to_dict() for p in available_printers]
|
|
|
|
# Job-Historie falls vorhanden
|
|
if guest_request.job:
|
|
job_data = guest_request.job.to_dict()
|
|
job_data["is_active"] = guest_request.job.status in ["scheduled", "running"]
|
|
job_data["is_overdue"] = guest_request.job.end_at < datetime.now() if guest_request.job.end_at else False
|
|
request_data["job_details"] = job_data
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"request": request_data
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen der Gastanfrage-Details: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
|
|
|
@guest_blueprint.route('/api/admin/requests/<int:request_id>/update', methods=['PUT'])
|
|
@approver_required
|
|
def api_update_request(request_id):
|
|
"""Gastanfrage aktualisieren (nur für Admins)."""
|
|
try:
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({"error": "Keine Daten erhalten"}), 400
|
|
|
|
with get_cached_session() as db_session:
|
|
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
|
if not guest_request:
|
|
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
|
|
|
# Erlaubte Felder für Updates
|
|
allowed_fields = ['printer_id', 'duration_min', 'approval_notes', 'rejection_reason']
|
|
changes_made = False
|
|
|
|
for field in allowed_fields:
|
|
if field in data:
|
|
if field == 'printer_id' and data[field]:
|
|
# Drucker validieren
|
|
printer = db_session.query(Printer).filter_by(id=data[field], active=True).first()
|
|
if not printer:
|
|
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
|
|
|
|
setattr(guest_request, field, data[field])
|
|
changes_made = True
|
|
|
|
if changes_made:
|
|
guest_request.processed_by = current_user.id
|
|
guest_request.processed_at = datetime.now()
|
|
db_session.commit()
|
|
|
|
logger.info(f"Gastanfrage {request_id} aktualisiert von Admin {current_user.id}")
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"message": "Anfrage erfolgreich aktualisiert",
|
|
"updated_by": current_user.name,
|
|
"updated_at": guest_request.processed_at.isoformat()
|
|
})
|
|
else:
|
|
return jsonify({"error": "Keine Änderungen vorgenommen"}), 400
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Aktualisieren der Gastanfrage: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
|
|
|
@guest_blueprint.route('/api/admin/requests/<int:request_id>', methods=['DELETE'])
|
|
@approver_required
|
|
def api_delete_request(request_id):
|
|
"""Gastanfrage löschen (nur für Admins)."""
|
|
try:
|
|
with get_cached_session() as db_session:
|
|
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
|
if not guest_request:
|
|
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
|
|
|
# Falls ein Job verknüpft ist, diesen auch löschen
|
|
if guest_request.job_id:
|
|
job = db_session.query(Job).filter_by(id=guest_request.job_id).first()
|
|
if job:
|
|
db_session.delete(job)
|
|
|
|
# Gastanfrage löschen
|
|
db_session.delete(guest_request)
|
|
db_session.commit()
|
|
|
|
logger.info(f"Gastanfrage {request_id} gelöscht von Admin {current_user.id} ({current_user.username})")
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"message": "Anfrage erfolgreich gelöscht",
|
|
"deleted_by": current_user.username,
|
|
"deleted_at": datetime.now().isoformat()
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Löschen der Gastanfrage: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
|
|
|
# Admin-Routen
|
|
@guest_blueprint.route('/admin/requests', methods=['GET'])
|
|
@approver_required
|
|
def admin_requests_management():
|
|
"""Admin-Oberfläche für die Verwaltung von Gastanfragen."""
|
|
return render_template('admin_guest_requests.html')
|
|
|
|
@guest_blueprint.route('/api/requests/<int:request_id>/approve', methods=['POST'])
|
|
@approver_required
|
|
def api_approve_request(request_id):
|
|
"""Gastanfrage genehmigen."""
|
|
try:
|
|
data = request.get_json() or {}
|
|
approval_notes = data.get('notes', '')
|
|
printer_id = data.get('printer_id') # Optional: Drucker zuweisen/ändern
|
|
|
|
with get_cached_session() as db_session:
|
|
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
|
if not guest_request:
|
|
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
|
|
|
if guest_request.status != "pending":
|
|
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
|
|
|
|
# ===== ERWEITERTE DRUCKER-VALIDIERUNG UND -ZUWEISUNG =====
|
|
from utils.hardware_integration import get_drucker_steuerung
|
|
drucker_steuerung = get_drucker_steuerung()
|
|
|
|
# Drucker validieren oder automatisch zuweisen
|
|
if printer_id:
|
|
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
|
|
if not printer:
|
|
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
|
|
|
|
# Status des ausgewählten Druckers prüfen
|
|
if printer.plug_ip:
|
|
try:
|
|
reachable, power_status = drucker_steuerung.check_outlet_status(
|
|
printer.plug_ip, printer.id
|
|
)
|
|
|
|
if not reachable:
|
|
return jsonify({
|
|
"error": f"Drucker '{printer.name}' ist derzeit offline und kann nicht zugewiesen werden",
|
|
"warning": "Der Drucker ist im Netzwerk nicht erreichbar. Die Genehmigung kann trotzdem erfolgen, aber der Job kann erst gestartet werden, wenn der Drucker wieder online ist."
|
|
}), 400
|
|
|
|
logger.info(f"✅ Admin-Drucker-Zuweisung validiert: {printer.name} (Status: {power_status})")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Status-Prüfung für zugewiesenen Drucker {printer.name} fehlgeschlagen: {e}")
|
|
# Warnung, aber Genehmigung trotzdem fortsetzen
|
|
|
|
guest_request.printer_id = printer_id
|
|
|
|
elif not guest_request.printer_id:
|
|
# Automatisch besten verfügbaren Drucker zuweisen
|
|
available_printers = db_session.query(Printer).filter(
|
|
Printer.active == True,
|
|
Printer.plug_ip.isnot(None)
|
|
).all()
|
|
|
|
if not available_printers:
|
|
return jsonify({
|
|
"error": "Kein aktiver Drucker mit Steckdosen-Steuerung verfügbar",
|
|
"details": "Bitte aktivieren Sie mindestens einen Drucker mit konfigurierter Tapo-Steckdose."
|
|
}), 400
|
|
|
|
# Status aller verfügbaren Drucker prüfen
|
|
online_printers = []
|
|
offline_printers = []
|
|
|
|
for printer in available_printers:
|
|
try:
|
|
reachable, power_status = drucker_steuerung.check_outlet_status(
|
|
printer.plug_ip, printer.id
|
|
)
|
|
|
|
if reachable:
|
|
online_printers.append({
|
|
'printer': printer,
|
|
'status': power_status
|
|
})
|
|
else:
|
|
offline_printers.append(printer)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Status-Prüfung für Drucker {printer.name} fehlgeschlagen: {e}")
|
|
offline_printers.append(printer)
|
|
|
|
# Bevorzuge online Drucker, aber fallback auf offline Drucker
|
|
if online_printers:
|
|
best_printer = online_printers[0]['printer']
|
|
guest_request.printer_id = best_printer.id
|
|
logger.info(f"✅ Automatisch ONLINE-Drucker zugewiesen: {best_printer.name} (Status: {online_printers[0]['status']})")
|
|
elif available_printers:
|
|
# Fallback: Ersten verfügbaren Drucker nehmen, auch wenn offline
|
|
fallback_printer = available_printers[0]
|
|
guest_request.printer_id = fallback_printer.id
|
|
logger.warning(f"⚠️ Automatisch OFFLINE-Drucker zugewiesen: {fallback_printer.name} (alle Drucker sind offline)")
|
|
else:
|
|
return jsonify({
|
|
"error": "Kein Drucker verfügbar",
|
|
"details": "Es sind keine aktiven Drucker konfiguriert."
|
|
}), 400
|
|
|
|
# Drucker-Objekt für Job-Erstellung laden
|
|
printer = db_session.query(Printer).filter_by(id=guest_request.printer_id).first()
|
|
if not printer:
|
|
return jsonify({"error": "Zugewiesener Drucker nicht gefunden"}), 400
|
|
|
|
# Anfrage genehmigen
|
|
guest_request.status = "approved"
|
|
guest_request.processed_by = current_user.id
|
|
guest_request.processed_at = datetime.now()
|
|
guest_request.approval_notes = approval_notes
|
|
|
|
# OTP generieren
|
|
otp_plain = guest_request.generate_otp()
|
|
|
|
# Zugehörigen Job erstellen
|
|
start_time = datetime.now() + timedelta(minutes=5) # Start in 5 Minuten
|
|
end_time = start_time + timedelta(minutes=guest_request.duration_min)
|
|
|
|
# Admin-Benutzer als Eigentümer verwenden
|
|
admin_user = db_session.query(User).filter_by(is_admin=True).first()
|
|
if not admin_user:
|
|
admin_user = current_user # Fallback auf aktuellen Benutzer
|
|
|
|
job = Job(
|
|
name=f"Gastauftrag: {guest_request.name}",
|
|
description=guest_request.reason or "Gastauftrag",
|
|
user_id=admin_user.id,
|
|
printer_id=guest_request.printer_id,
|
|
start_at=start_time,
|
|
end_at=end_time,
|
|
status="scheduled",
|
|
duration_minutes=guest_request.duration_min,
|
|
owner_id=admin_user.id
|
|
)
|
|
|
|
db_session.add(job)
|
|
db_session.flush() # ID generieren
|
|
|
|
# Job-ID in Gastanfrage speichern
|
|
guest_request.job_id = job.id
|
|
|
|
db_session.commit()
|
|
|
|
logger.info(f"Gastanfrage {request_id} genehmigt von Admin {current_user.id} ({current_user.username}), Drucker: {printer.name}")
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"status": "approved",
|
|
"job_id": job.id,
|
|
"otp": otp_plain, # Nur in dieser Antwort wird der OTP-Klartext zurückgegeben
|
|
"otp_code": otp_plain, # Für Frontend-Kompatibilität
|
|
"printer_name": printer.name,
|
|
"printer_id": printer.id,
|
|
"approved_by": current_user.username,
|
|
"approved_at": guest_request.processed_at.isoformat(),
|
|
"notes": approval_notes,
|
|
"message": f"Anfrage genehmigt. Zugangscode: {otp_plain}. Drucker: {printer.name}"
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Genehmigen der Gastanfrage: {str(e)}")
|
|
return jsonify({"error": f"Fehler beim Verarbeiten der Anfrage: {str(e)}"}), 500
|
|
|
|
@guest_blueprint.route('/api/requests/<int:request_id>/deny', methods=['POST'])
|
|
@approver_required
|
|
def api_deny_request(request_id):
|
|
"""Gastanfrage ablehnen mit robuster Fehlerbehandlung."""
|
|
try:
|
|
# Content-Type prüfen
|
|
if not request.is_json and request.content_type != 'application/json':
|
|
return jsonify({"error": "Content-Type muss application/json sein"}), 400
|
|
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({"error": "Keine JSON-Daten erhalten"}), 400
|
|
|
|
# Reason validation verbessert
|
|
rejection_reason = data.get('reason', '').strip()
|
|
if not rejection_reason or len(rejection_reason) < 3:
|
|
return jsonify({
|
|
"error": "Ablehnungsgrund ist erforderlich (mindestens 3 Zeichen)",
|
|
"field": "reason"
|
|
}), 400
|
|
|
|
with get_cached_session() as db_session:
|
|
try:
|
|
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
|
if not guest_request:
|
|
return jsonify({
|
|
"error": "Anfrage nicht gefunden",
|
|
"request_id": request_id
|
|
}), 404
|
|
|
|
# Status-Validation verbessert
|
|
if guest_request.status not in ["pending"]:
|
|
return jsonify({
|
|
"error": f"Anfrage kann im Status '{guest_request.status}' nicht abgelehnt werden",
|
|
"current_status": guest_request.status,
|
|
"allowed_statuses": ["pending"]
|
|
}), 400
|
|
|
|
# Transaktion mit besserer Fehlerbehandlung
|
|
guest_request.status = "denied"
|
|
guest_request.processed_by = current_user.id
|
|
guest_request.processed_at = datetime.now()
|
|
guest_request.rejection_reason = rejection_reason
|
|
|
|
# Commit mit expliziter Fehlerbehandlung
|
|
db_session.commit()
|
|
|
|
logger.info(f"Gastanfrage {request_id} abgelehnt von Admin {current_user.id} ({current_user.username}): {rejection_reason}")
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"status": "denied",
|
|
"request_id": request_id,
|
|
"rejected_by": current_user.username,
|
|
"rejected_at": guest_request.processed_at.isoformat(),
|
|
"reason": rejection_reason,
|
|
"message": "Anfrage wurde erfolgreich abgelehnt"
|
|
})
|
|
|
|
except Exception as db_error:
|
|
db_session.rollback()
|
|
logger.error(f"Datenbankfehler beim Ablehnen der Anfrage {request_id}: {str(db_error)}")
|
|
return jsonify({
|
|
"error": "Datenbankfehler beim Verarbeiten der Anfrage",
|
|
"details": str(db_error) if current_app.debug else None
|
|
}), 500
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unerwarteter Fehler beim Ablehnen der Gastanfrage {request_id}: {str(e)}")
|
|
return jsonify({
|
|
"error": "Unerwarteter Serverfehler",
|
|
"details": str(e) if current_app.debug else None
|
|
}), 500
|
|
|
|
@guest_blueprint.route('/api/admin/requests/<int:request_id>/otp', methods=['GET'])
|
|
@approver_required
|
|
def api_get_request_otp(request_id):
|
|
"""OTP-Code für genehmigte Gastanfrage abrufen (nur für Admins)."""
|
|
try:
|
|
with get_cached_session() as db_session:
|
|
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
|
if not guest_request:
|
|
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
|
|
|
if guest_request.status != "approved":
|
|
return jsonify({"error": "Anfrage ist nicht genehmigt"}), 400
|
|
|
|
if not guest_request.otp_code:
|
|
return jsonify({"error": "Kein OTP-Code verfügbar"}), 400
|
|
|
|
# Prüfen ob OTP noch gültig ist
|
|
if guest_request.otp_expires_at and guest_request.otp_expires_at < datetime.now():
|
|
return jsonify({
|
|
"error": "OTP-Code ist abgelaufen",
|
|
"expired": True,
|
|
"expired_at": guest_request.otp_expires_at.isoformat()
|
|
}), 400
|
|
|
|
# Prüfen ob OTP bereits verwendet wurde
|
|
otp_used = guest_request.otp_used_at is not None
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"request_id": request_id,
|
|
"has_otp": True,
|
|
"otp_used": otp_used,
|
|
"otp_used_at": guest_request.otp_used_at.isoformat() if guest_request.otp_used_at else None,
|
|
"otp_expires_at": guest_request.otp_expires_at.isoformat() if guest_request.otp_expires_at else None,
|
|
"job_id": guest_request.job_id,
|
|
"message": "OTP-Code wurde bei Genehmigung angezeigt und kann nicht erneut abgerufen werden" if otp_used else "OTP-Code ist bereit zur Verwendung"
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen des OTP-Codes: {str(e)}")
|
|
return jsonify({"error": "Fehler beim Abrufen des OTP-Codes"}), 500
|
|
|
|
@guest_blueprint.route('/api/guest/status', methods=['POST'])
|
|
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
|
|
def api_guest_status_by_otp():
|
|
"""
|
|
Öffentliche Route für Gäste um ihren Auftragsstatus mit Name + OTP-Code zu prüfen.
|
|
Keine Authentifizierung erforderlich (Offline-System).
|
|
"""
|
|
try:
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': 'Keine Daten empfangen'
|
|
}), 400
|
|
|
|
otp_code = data.get('otp_code', '').strip()
|
|
name = data.get('name', '').strip()
|
|
|
|
if not otp_code:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': 'OTP-Code ist erforderlich'
|
|
}), 400
|
|
|
|
if not name:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': 'Name ist erforderlich'
|
|
}), 400
|
|
|
|
with get_cached_session() as db_session:
|
|
# Gastanfrage mit Name + OTP-Code finden (sichere Methode)
|
|
found_request = GuestRequest.find_by_otp_and_name(otp_code, name)
|
|
|
|
if not found_request:
|
|
logger.warning(f"Ungültiger OTP-Code oder Name für Gast-Status-Abfrage: {name} / {otp_code[:4]}****")
|
|
return jsonify({
|
|
'success': False,
|
|
'message': 'Ungültiger Code oder Name stimmt nicht überein'
|
|
}), 404
|
|
|
|
# Status-Informationen für den Gast zusammenstellen
|
|
status_info = {
|
|
'id': found_request.id,
|
|
'name': found_request.name,
|
|
'file_name': found_request.file_name,
|
|
'status': found_request.status,
|
|
'created_at': found_request.created_at.isoformat() if found_request.created_at else None,
|
|
'updated_at': found_request.updated_at.isoformat() if found_request.updated_at else None,
|
|
'duration_min': found_request.duration_min,
|
|
'reason': found_request.reason
|
|
}
|
|
|
|
# Status-spezifische Informationen hinzufügen
|
|
if found_request.status == 'approved':
|
|
status_info.update({
|
|
'approved_at': found_request.approved_at.isoformat() if found_request.approved_at else None,
|
|
'approval_notes': found_request.approval_notes,
|
|
'message': 'Ihr Auftrag wurde genehmigt! Sie können mit dem Drucken beginnen.',
|
|
'can_start_job': found_request.otp_used_at is None # Noch nicht verwendet
|
|
})
|
|
|
|
# Job-Informationen hinzufügen falls vorhanden
|
|
if found_request.job_id:
|
|
job = db_session.query(Job).options(joinedload(Job.printer)).filter_by(id=found_request.job_id).first()
|
|
if job:
|
|
status_info['job'] = {
|
|
'id': job.id,
|
|
'name': job.name,
|
|
'status': job.status,
|
|
'start_at': job.start_at.isoformat() if job.start_at else None,
|
|
'end_at': job.end_at.isoformat() if job.end_at else None,
|
|
'printer_name': job.printer.name if job.printer else None
|
|
}
|
|
|
|
elif found_request.status == 'rejected':
|
|
status_info.update({
|
|
'rejected_at': found_request.rejected_at.isoformat() if found_request.rejected_at else None,
|
|
'rejection_reason': found_request.rejection_reason,
|
|
'message': 'Ihr Auftrag wurde leider abgelehnt.'
|
|
})
|
|
|
|
elif found_request.status == 'pending':
|
|
# Berechne wie lange der Auftrag schon wartet
|
|
if found_request.created_at:
|
|
waiting_time = datetime.now() - found_request.created_at
|
|
hours_waiting = int(waiting_time.total_seconds() / 3600)
|
|
status_info.update({
|
|
'hours_waiting': hours_waiting,
|
|
'message': f'Ihr Auftrag wird bearbeitet. Wartezeit: {hours_waiting} Stunden.'
|
|
})
|
|
else:
|
|
status_info['message'] = 'Ihr Auftrag wird bearbeitet.'
|
|
|
|
# OTP als verwendet markieren (da erfolgreich abgefragt)
|
|
db_session.commit()
|
|
|
|
logger.info(f"Gast-Status-Abfrage erfolgreich für Request {found_request.id}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'request': status_info
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei Gast-Status-Abfrage: {str(e)}")
|
|
return jsonify({
|
|
'success': False,
|
|
'message': 'Fehler beim Abrufen des Status'
|
|
}), 500
|
|
|
|
@guest_blueprint.route('/status-check')
|
|
def guest_status_check_page():
|
|
"""Status-Check-Seite für Gäste."""
|
|
return render_template('guest_status_check.html')
|
|
|
|
@guest_blueprint.route('/api/admin/printer-status', methods=['GET'])
|
|
@approver_required
|
|
def api_get_printer_status_for_admin():
|
|
"""
|
|
Drucker-Status für Admin-Oberfläche abrufen.
|
|
Zeigt welche Drucker verfügbar sind für die Zuweisung bei Gastanfragen.
|
|
"""
|
|
try:
|
|
from utils.hardware_integration import get_drucker_steuerung
|
|
|
|
with get_cached_session() as db_session:
|
|
# Alle aktiven Drucker laden
|
|
printers = db_session.query(Printer).filter(Printer.active == True).all()
|
|
|
|
if not printers:
|
|
return jsonify({
|
|
"success": True,
|
|
"printers": [],
|
|
"summary": {
|
|
"total": 0,
|
|
"online": 0,
|
|
"offline": 0,
|
|
"unconfigured": 0
|
|
},
|
|
"message": "Keine aktiven Drucker konfiguriert"
|
|
})
|
|
|
|
drucker_steuerung = get_drucker_steuerung()
|
|
printer_status_list = []
|
|
|
|
summary = {
|
|
"total": len(printers),
|
|
"online": 0,
|
|
"offline": 0,
|
|
"unconfigured": 0
|
|
}
|
|
|
|
for printer in printers:
|
|
printer_info = {
|
|
"id": printer.id,
|
|
"name": printer.name,
|
|
"model": printer.model or "Unbekannt",
|
|
"location": printer.location or "TBA Marienfelde",
|
|
"ip_address": printer.ip_address,
|
|
"plug_ip": printer.plug_ip,
|
|
"active": printer.active,
|
|
"status": "unknown",
|
|
"reachable": False,
|
|
"power_state": "unknown",
|
|
"can_be_assigned": False,
|
|
"status_message": "",
|
|
"last_checked": datetime.now().isoformat()
|
|
}
|
|
|
|
if printer.plug_ip:
|
|
try:
|
|
# Echtzeit-Status prüfen
|
|
reachable, power_status = drucker_steuerung.check_outlet_status(
|
|
printer.plug_ip, printer.id
|
|
)
|
|
|
|
printer_info["reachable"] = reachable
|
|
printer_info["power_state"] = power_status
|
|
|
|
if reachable:
|
|
printer_info["status"] = "online"
|
|
printer_info["can_be_assigned"] = True
|
|
printer_info["status_message"] = f"Online ({power_status.upper()})"
|
|
summary["online"] += 1
|
|
else:
|
|
printer_info["status"] = "offline"
|
|
printer_info["can_be_assigned"] = False
|
|
printer_info["status_message"] = "Offline - Nicht erreichbar"
|
|
summary["offline"] += 1
|
|
|
|
except Exception as e:
|
|
printer_info["status"] = "error"
|
|
printer_info["can_be_assigned"] = False
|
|
printer_info["status_message"] = f"Fehler: {str(e)}"
|
|
printer_info["error"] = str(e)
|
|
summary["offline"] += 1
|
|
|
|
logger.warning(f"⚠️ Admin-Status-Prüfung für {printer.name} fehlgeschlagen: {e}")
|
|
else:
|
|
printer_info["status"] = "unconfigured"
|
|
printer_info["can_be_assigned"] = False
|
|
printer_info["status_message"] = "Keine Steckdose konfiguriert"
|
|
summary["unconfigured"] += 1
|
|
|
|
printer_status_list.append(printer_info)
|
|
|
|
# Nach Status sortieren: Online zuerst, dann offline, dann unkonfiguriert
|
|
printer_status_list.sort(key=lambda x: (
|
|
0 if x["status"] == "online" else
|
|
1 if x["status"] == "offline" else
|
|
2 # unconfigured oder error
|
|
))
|
|
|
|
# Empfehlung für Admin generieren
|
|
recommendations = []
|
|
if summary["online"] == 0:
|
|
if summary["unconfigured"] > 0:
|
|
recommendations.append("Konfigurieren Sie Tapo-Steckdosen für die Drucker ohne Steuerung")
|
|
if summary["offline"] > 0:
|
|
recommendations.append("Prüfen Sie die Netzwerkverbindungen der offline Drucker")
|
|
recommendations.append("Aktuell können keine Gastanfragen automatisch gestartet werden")
|
|
elif summary["online"] < summary["total"]:
|
|
recommendations.append(f"Nur {summary['online']} von {summary['total']} Druckern sind verfügbar")
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"printers": printer_status_list,
|
|
"summary": summary,
|
|
"recommendations": recommendations,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"message": f"Status von {len(printer_status_list)} Druckern abgerufen"
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen des Admin-Drucker-Status: {str(e)}")
|
|
return jsonify({
|
|
"success": False,
|
|
"error": "Fehler beim Abrufen des Drucker-Status",
|
|
"details": str(e)
|
|
}), 500 |