"feat: Implement guest job application flow"
This commit is contained in:
parent
c2d8c795f1
commit
96ae5730b8
@ -1,4 +1,6 @@
|
||||
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
|
||||
@ -41,6 +43,36 @@ def guest_request_form():
|
||||
db_session.expunge_all()
|
||||
return render_template('guest_request.html', printers=printers)
|
||||
|
||||
@guest_blueprint.route('/start-job', methods=['GET'])
|
||||
def guest_start_job_form():
|
||||
"""Code-Eingabe-Formular für Gäste anzeigen."""
|
||||
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."""
|
||||
@ -75,23 +107,10 @@ def guest_requests_overview():
|
||||
|
||||
# Grund zensieren (nur erste 20 Zeichen anzeigen)
|
||||
censored_reason = "***"
|
||||
if req.reason:
|
||||
if len(req.reason) > 20:
|
||||
censored_reason = req.reason[:20] + "***"
|
||||
else:
|
||||
censored_reason = req.reason[:10] + "***" if len(req.reason) > 10 else "***"
|
||||
|
||||
# Drucker-Info laden (jetzt durch eager loading verfügbar)
|
||||
printer_name = "Unbekannt"
|
||||
if req.printer:
|
||||
printer_name = req.printer.name
|
||||
|
||||
# Job-Status laden, falls vorhanden
|
||||
job_status = None
|
||||
if req.job_id:
|
||||
job = db_session.query(Job).filter_by(id=req.job_id).first()
|
||||
if job:
|
||||
job_status = job.status
|
||||
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,
|
||||
@ -101,17 +120,17 @@ def guest_requests_overview():
|
||||
"duration_min": req.duration_min,
|
||||
"created_at": req.created_at,
|
||||
"status": req.status,
|
||||
"printer_name": printer_name,
|
||||
"job_status": job_status
|
||||
"printer": req.printer.to_dict() if req.printer else None
|
||||
})
|
||||
|
||||
logger.info(f"Öffentliche Druckanträge-Übersicht aufgerufen - {len(public_requests)} Einträge")
|
||||
|
||||
|
||||
# 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 Druckanträge-Übersicht: {str(e)}")
|
||||
return render_template('guest_requests_overview.html', requests=[], error="Fehler beim Laden der Daten")
|
||||
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):
|
||||
@ -125,11 +144,18 @@ def guest_request_status(request_id):
|
||||
if not guest_request:
|
||||
abort(404, "Anfrage nicht gefunden")
|
||||
|
||||
# Nur wenn Status "approved" ist, OTP generieren und anzeigen
|
||||
# OTP-Code nur anzeigen, wenn Anfrage genehmigt wurde
|
||||
otp_code = None
|
||||
if guest_request.status == "approved" and not guest_request.otp_code:
|
||||
otp_code = guest_request.generate_otp()
|
||||
db_session.commit()
|
||||
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
|
||||
@ -144,7 +170,8 @@ def guest_request_status(request_id):
|
||||
return render_template('guest_status.html',
|
||||
request=guest_request,
|
||||
job=job,
|
||||
otp_code=otp_code)
|
||||
otp_code=otp_code,
|
||||
show_start_link=show_start_link)
|
||||
|
||||
# API-Endpunkte
|
||||
@guest_blueprint.route('/api/guest/requests', methods=['POST'])
|
||||
@ -213,6 +240,103 @@ def api_create_guest_request():
|
||||
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'])
|
||||
def api_start_job_with_code():
|
||||
"""Job mit OTP-Code starten."""
|
||||
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:
|
||||
# Alle genehmigten Gastanfragen mit OTP-Codes finden
|
||||
guest_requests = db_session.query(GuestRequest).filter(
|
||||
GuestRequest.status == "approved",
|
||||
GuestRequest.otp_code.isnot(None),
|
||||
GuestRequest.otp_used_at.is_(None) # Noch nicht verwendet
|
||||
).all()
|
||||
|
||||
matching_request = None
|
||||
for req in guest_requests:
|
||||
# Code validieren
|
||||
if req.verify_otp(code):
|
||||
matching_request = req
|
||||
break
|
||||
|
||||
if not matching_request:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Ungültiger oder bereits verwendeter Code"
|
||||
}), 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
|
||||
|
||||
# Job starten
|
||||
now = datetime.now()
|
||||
job.status = "running"
|
||||
job.start_at = now
|
||||
job.end_at = now + timedelta(minutes=matching_request.duration_min)
|
||||
job.actual_start_time = now
|
||||
|
||||
# OTP als verwendet markieren
|
||||
matching_request.otp_used_at = now
|
||||
|
||||
# Drucker einschalten (falls implementiert)
|
||||
if job.printer and job.printer.plug_ip:
|
||||
try:
|
||||
from utils.job_scheduler import toggle_plug
|
||||
toggle_plug(job.printer_id, True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Einschalten des Druckers: {str(e)}")
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Job {job.id} mit OTP-Code gestartet für Gastanfrage {matching_request.id}")
|
||||
|
||||
return jsonify({
|
||||
"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,
|
||||
"printer_name": job.printer.name if job.printer else "Unbekannt",
|
||||
"message": f"Job '{job.name}' erfolgreich gestartet"
|
||||
})
|
||||
|
||||
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."""
|
||||
@ -232,191 +356,73 @@ def api_get_guest_request(request_id):
|
||||
logger.error(f"Fehler beim Abrufen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@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
|
||||
|
||||
# Drucker validieren, falls angegeben
|
||||
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
|
||||
guest_request.printer_id = printer_id
|
||||
|
||||
# Sicherstellen, dass ein Drucker zugewiesen ist
|
||||
if not guest_request.printer_id:
|
||||
return jsonify({"error": "Kein Drucker zugewiesen. Bitte wählen Sie einen Drucker aus."}), 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(role="admin").first()
|
||||
|
||||
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.name})")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": "approved",
|
||||
"job_id": job.id,
|
||||
"otp": otp_plain, # Nur in dieser Antwort wird der OTP-Klartext zurückgegeben
|
||||
"approved_by": current_user.name,
|
||||
"approved_at": guest_request.processed_at.isoformat(),
|
||||
"notes": approval_notes
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Genehmigen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/requests/<int:request_id>/deny', methods=['POST'])
|
||||
@approver_required
|
||||
def api_deny_request(request_id):
|
||||
"""Gastanfrage ablehnen."""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
rejection_reason = data.get('reason', '')
|
||||
|
||||
if not rejection_reason.strip():
|
||||
return jsonify({"error": "Ablehnungsgrund ist erforderlich"}), 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
|
||||
|
||||
if guest_request.status != "pending":
|
||||
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
|
||||
|
||||
# Anfrage ablehnen
|
||||
guest_request.status = "denied"
|
||||
guest_request.processed_by = current_user.id
|
||||
guest_request.processed_at = datetime.now()
|
||||
guest_request.rejection_reason = rejection_reason
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Gastanfrage {request_id} abgelehnt von Admin {current_user.id} ({current_user.name}): {rejection_reason}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": "denied",
|
||||
"rejected_by": current_user.name,
|
||||
"rejected_at": guest_request.processed_at.isoformat(),
|
||||
"reason": rejection_reason
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ablehnen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/jobs/start/<string:otp>', methods=['POST'])
|
||||
def api_start_job_with_otp(otp):
|
||||
"""Job mit OTP starten."""
|
||||
@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:
|
||||
# Alle genehmigten Anfragen mit OTP durchsuchen
|
||||
guest_requests = db_session.query(GuestRequest).filter_by(status="approved").all()
|
||||
# Job mit Drucker-Information laden
|
||||
job = db_session.query(Job).options(
|
||||
joinedload(Job.printer)
|
||||
).filter_by(id=job_id).first()
|
||||
|
||||
valid_request = None
|
||||
for req in guest_requests:
|
||||
if req.verify_otp(otp):
|
||||
valid_request = req
|
||||
break
|
||||
|
||||
if not valid_request:
|
||||
return jsonify({"error": "Ungültiger oder abgelaufener Code"}), 400
|
||||
|
||||
if not valid_request.job_id:
|
||||
return jsonify({"error": "Kein Job mit diesem Code verknüpft"}), 400
|
||||
|
||||
# Job laden
|
||||
job = db_session.query(Job).filter_by(id=valid_request.job_id).first()
|
||||
if not job:
|
||||
return jsonify({"error": "Job nicht gefunden"}), 404
|
||||
|
||||
# Job-Status prüfen
|
||||
if job.status != "scheduled":
|
||||
return jsonify({"error": "Job kann nicht gestartet werden"}), 400
|
||||
# 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
|
||||
|
||||
# Grace-Period prüfen (5 Minuten vor bis 5 Minuten nach geplantem Start)
|
||||
# Aktuelle Zeit für Berechnungen
|
||||
now = datetime.now()
|
||||
start_time = job.start_at
|
||||
grace_start = start_time - timedelta(minutes=5)
|
||||
grace_end = start_time + timedelta(minutes=5)
|
||||
|
||||
if now < grace_start:
|
||||
return jsonify({
|
||||
"error": f"Der Job kann erst ab {grace_start.strftime('%H:%M')} Uhr gestartet werden"
|
||||
}), 400
|
||||
# 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))
|
||||
|
||||
if now > job.end_at:
|
||||
return jsonify({"error": "Der Job ist bereits abgelaufen"}), 400
|
||||
# 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 starten
|
||||
job.status = "active"
|
||||
job.start_at = now # Aktualisiere Startzeit auf jetzt
|
||||
|
||||
# OTP als verwendet markieren
|
||||
valid_request.otp_used_at = now
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Job {job.id} mit OTP {otp} gestartet von IP: {request.remote_addr}")
|
||||
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_id": job.id,
|
||||
"status": job.status,
|
||||
"started_at": job.start_at.isoformat(),
|
||||
"end_at": job.end_at.isoformat()
|
||||
"job": job_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Starten des Jobs mit OTP: {str(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'])
|
||||
@ -640,4 +646,129 @@ def api_update_request(request_id):
|
||||
@approver_required
|
||||
def admin_requests_management():
|
||||
"""Admin-Oberfläche für die Verwaltung von Gastanfragen."""
|
||||
return render_template('admin_guest_requests.html')
|
||||
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
|
||||
|
||||
# Drucker validieren, falls angegeben
|
||||
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
|
||||
guest_request.printer_id = printer_id
|
||||
|
||||
# Sicherstellen, dass ein Drucker zugewiesen ist
|
||||
if not guest_request.printer_id:
|
||||
return jsonify({"error": "Kein Drucker zugewiesen. Bitte wählen Sie einen Drucker aus."}), 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})")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": "approved",
|
||||
"job_id": job.id,
|
||||
"otp": otp_plain, # Nur in dieser Antwort wird der OTP-Klartext zurückgegeben
|
||||
"approved_by": current_user.username,
|
||||
"approved_at": guest_request.processed_at.isoformat(),
|
||||
"notes": approval_notes,
|
||||
"message": f"Anfrage genehmigt. Zugangscode: {otp_plain}"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Genehmigen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/requests/<int:request_id>/deny', methods=['POST'])
|
||||
@approver_required
|
||||
def api_deny_request(request_id):
|
||||
"""Gastanfrage ablehnen."""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
rejection_reason = data.get('reason', '')
|
||||
|
||||
if not rejection_reason.strip():
|
||||
return jsonify({"error": "Ablehnungsgrund ist erforderlich"}), 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
|
||||
|
||||
if guest_request.status != "pending":
|
||||
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
|
||||
|
||||
# Anfrage ablehnen
|
||||
guest_request.status = "denied"
|
||||
guest_request.processed_by = current_user.id
|
||||
guest_request.processed_at = datetime.now()
|
||||
guest_request.rejection_reason = rejection_reason
|
||||
|
||||
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",
|
||||
"rejected_by": current_user.username,
|
||||
"rejected_at": guest_request.processed_at.isoformat(),
|
||||
"reason": rejection_reason,
|
||||
"message": "Anfrage wurde abgelehnt"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ablehnen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
@ -819,21 +819,28 @@ class GuestRequest(Base):
|
||||
|
||||
def verify_otp(self, otp_plain: str) -> bool:
|
||||
"""
|
||||
Überprüft, ob der angegebene OTP-Code gültig ist.
|
||||
Verifiziert einen OTP-Code gegen den gespeicherten Hash.
|
||||
|
||||
Args:
|
||||
otp_plain: Der zu überprüfende OTP-Code im Klartext
|
||||
otp_plain: Der zu prüfende OTP-Code im Klartext
|
||||
|
||||
Returns:
|
||||
bool: True, wenn der Code gültig ist, sonst False
|
||||
bool: True wenn der Code korrekt ist, False andernfalls
|
||||
"""
|
||||
if not self.otp_code:
|
||||
if not self.otp_code or not otp_plain:
|
||||
return False
|
||||
|
||||
otp_bytes = otp_plain.encode('utf-8')
|
||||
hash_bytes = self.otp_code.encode('utf-8')
|
||||
|
||||
return bcrypt.checkpw(otp_bytes, hash_bytes)
|
||||
try:
|
||||
# Code normalisieren (Großbuchstaben)
|
||||
otp_plain = otp_plain.upper().strip()
|
||||
|
||||
# Hash verifizieren
|
||||
otp_bytes = otp_plain.encode('utf-8')
|
||||
stored_hash = self.otp_code.encode('utf-8')
|
||||
|
||||
return bcrypt.checkpw(otp_bytes, stored_hash)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ===== DATENBANK-INITIALISIERUNG MIT OPTIMIERUNGEN =====
|
||||
|
@ -1 +1,699 @@
|
||||
|
||||
/**
|
||||
* Mercedes-Benz MYP Platform - Professional Theme
|
||||
* Professionelle Light/Dark Mode Implementierung
|
||||
*/
|
||||
|
||||
/* Globale CSS-Variablen für konsistente Theming */
|
||||
:root {
|
||||
/* Mercedes-Benz Markenfarben */
|
||||
--mb-primary: #3b82f6;
|
||||
--mb-primary-dark: #1d4ed8;
|
||||
--mb-secondary: #64748b;
|
||||
--mb-accent: #0ea5e9;
|
||||
|
||||
/* Light Mode Farbpalette */
|
||||
--light-bg-primary: #ffffff;
|
||||
--light-bg-secondary: #f8fafc;
|
||||
--light-bg-tertiary: #f1f5f9;
|
||||
--light-surface: #ffffff;
|
||||
--light-surface-hover: #f8fafc;
|
||||
--light-text-primary: #0f172a;
|
||||
--light-text-secondary: #475569;
|
||||
--light-text-muted: #64748b;
|
||||
--light-border: #e2e8f0;
|
||||
--light-border-strong: #cbd5e1;
|
||||
--light-shadow: rgba(0, 0, 0, 0.1);
|
||||
--light-shadow-strong: rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* Dark Mode Farbpalette */
|
||||
--dark-bg-primary: #0f172a;
|
||||
--dark-bg-secondary: #1e293b;
|
||||
--dark-bg-tertiary: #334155;
|
||||
--dark-surface: #1e293b;
|
||||
--dark-surface-hover: #334155;
|
||||
--dark-text-primary: #f8fafc;
|
||||
--dark-text-secondary: #e2e8f0;
|
||||
--dark-text-muted: #94a3b8;
|
||||
--dark-border: #334155;
|
||||
--dark-border-strong: #475569;
|
||||
--dark-shadow: rgba(0, 0, 0, 0.3);
|
||||
--dark-shadow-strong: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Professionelle Hero-Header Stile */
|
||||
.professional-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 2rem;
|
||||
margin: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
background: linear-gradient(135deg, var(--light-bg-secondary) 0%, var(--light-bg-tertiary) 100%);
|
||||
border: 1px solid var(--light-border);
|
||||
box-shadow: 0 20px 40px var(--light-shadow);
|
||||
}
|
||||
|
||||
.dark .professional-hero {
|
||||
background: linear-gradient(135deg, var(--dark-bg-primary) 0%, var(--dark-bg-secondary) 100%);
|
||||
border: 1px solid var(--dark-border);
|
||||
box-shadow: 0 20px 40px var(--dark-shadow-strong);
|
||||
}
|
||||
|
||||
.professional-hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dark .professional-hero::before {
|
||||
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.05) 50%, transparent 70%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Hero Pattern Overlay */
|
||||
.hero-pattern {
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 20%, var(--light-border) 1px, transparent 1px),
|
||||
radial-gradient(circle at 80% 80%, var(--light-border) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
background-position: 0 0, 25px 25px;
|
||||
}
|
||||
|
||||
.dark .hero-pattern {
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 20%, var(--dark-border) 1px, transparent 1px),
|
||||
radial-gradient(circle at 80% 80%, var(--dark-border) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
/* Professionelle Container */
|
||||
.professional-container {
|
||||
background: var(--light-surface);
|
||||
border: 1px solid var(--light-border);
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 10px 30px var(--light-shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .professional-container {
|
||||
background: var(--dark-surface);
|
||||
border: 1px solid var(--dark-border);
|
||||
box-shadow: 0 10px 30px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.professional-container:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px var(--light-shadow-strong);
|
||||
}
|
||||
|
||||
.dark .professional-container:hover {
|
||||
box-shadow: 0 20px 40px var(--dark-shadow-strong);
|
||||
}
|
||||
|
||||
/* Mercedes-Benz Glassmorphism Effekt */
|
||||
.mb-glass {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .mb-glass {
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.mb-glass:hover {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.dark .mb-glass:hover {
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Professional Buttons */
|
||||
.btn-professional {
|
||||
background: linear-gradient(135deg, var(--mb-primary) 0%, var(--mb-primary-dark) 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.025em;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.btn-professional::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-professional:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-professional:hover {
|
||||
background: linear-gradient(135deg, var(--mb-primary-dark) 0%, #1e40af 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-professional:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Secondary Button Style */
|
||||
.btn-secondary-professional {
|
||||
background: var(--light-surface);
|
||||
color: var(--light-text-primary);
|
||||
border: 2px solid var(--light-border-strong);
|
||||
border-radius: 1rem;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px var(--light-shadow);
|
||||
}
|
||||
|
||||
.dark .btn-secondary-professional {
|
||||
background: var(--dark-surface);
|
||||
color: var(--dark-text-primary);
|
||||
border-color: var(--dark-border-strong);
|
||||
box-shadow: 0 4px 15px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.btn-secondary-professional:hover {
|
||||
background: var(--light-surface-hover);
|
||||
border-color: var(--mb-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px var(--light-shadow-strong);
|
||||
}
|
||||
|
||||
.dark .btn-secondary-professional:hover {
|
||||
background: var(--dark-surface-hover);
|
||||
box-shadow: 0 8px 25px var(--dark-shadow);
|
||||
}
|
||||
|
||||
/* Professional Input Fields */
|
||||
.input-professional {
|
||||
background: var(--light-surface);
|
||||
border: 2px solid var(--light-border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.875rem 1rem;
|
||||
color: var(--light-text-primary);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px var(--light-shadow);
|
||||
}
|
||||
|
||||
.dark .input-professional {
|
||||
background: var(--dark-surface);
|
||||
border-color: var(--dark-border);
|
||||
color: var(--dark-text-primary);
|
||||
box-shadow: 0 2px 8px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.input-professional:focus {
|
||||
border-color: var(--mb-primary);
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.input-professional::placeholder {
|
||||
color: var(--light-text-muted);
|
||||
}
|
||||
|
||||
.dark .input-professional::placeholder {
|
||||
color: var(--dark-text-muted);
|
||||
}
|
||||
|
||||
/* Professional Cards */
|
||||
.card-professional {
|
||||
background: var(--light-surface);
|
||||
border: 1px solid var(--light-border);
|
||||
border-radius: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 20px var(--light-shadow);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .card-professional {
|
||||
background: var(--dark-surface);
|
||||
border-color: var(--dark-border);
|
||||
box-shadow: 0 4px 20px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.card-professional::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--mb-primary), var(--mb-accent));
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card-professional:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.card-professional:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px var(--light-shadow-strong);
|
||||
}
|
||||
|
||||
.dark .card-professional:hover {
|
||||
box-shadow: 0 12px 40px var(--dark-shadow-strong);
|
||||
}
|
||||
|
||||
/* Professional Statistics Cards */
|
||||
.stat-card {
|
||||
background: var(--light-surface);
|
||||
border: 1px solid var(--light-border);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px var(--light-shadow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .stat-card {
|
||||
background: var(--dark-surface);
|
||||
border-color: var(--dark-border);
|
||||
box-shadow: 0 4px 15px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 8px 30px var(--light-shadow-strong);
|
||||
}
|
||||
|
||||
.dark .stat-card:hover {
|
||||
box-shadow: 0 8px 30px var(--dark-shadow-strong);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--light-text-primary);
|
||||
line-height: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dark .stat-number {
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--light-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.dark .stat-label {
|
||||
color: var(--dark-text-muted);
|
||||
}
|
||||
|
||||
/* Professional Status Badges */
|
||||
.status-professional {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.status-professional:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Status-spezifische Farben */
|
||||
.status-pending {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
color: #92400e;
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
.dark .status-pending {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #065f46;
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.dark .status-approved {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-denied {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #991b1b;
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.dark .status-denied {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Professional Typography */
|
||||
.title-professional {
|
||||
background: linear-gradient(135deg, var(--light-text-primary) 0%, var(--light-text-secondary) 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.dark .title-professional {
|
||||
background: linear-gradient(135deg, var(--dark-text-primary) 0%, var(--dark-text-secondary) 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle-professional {
|
||||
color: var(--light-text-muted);
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.dark .subtitle-professional {
|
||||
color: var(--dark-text-muted);
|
||||
}
|
||||
|
||||
/* Professional Navigation */
|
||||
.nav-professional {
|
||||
background: var(--light-surface);
|
||||
border: 1px solid var(--light-border);
|
||||
border-radius: 1rem;
|
||||
padding: 0.5rem;
|
||||
box-shadow: 0 4px 15px var(--light-shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.dark .nav-professional {
|
||||
background: var(--dark-surface);
|
||||
border-color: var(--dark-border);
|
||||
box-shadow: 0 4px 15px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.nav-item-professional {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--light-text-secondary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dark .nav-item-professional {
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
.nav-item-professional:hover {
|
||||
background: var(--light-surface-hover);
|
||||
color: var(--light-text-primary);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.dark .nav-item-professional:hover {
|
||||
background: var(--dark-surface-hover);
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
.nav-item-professional.active {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--mb-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark .nav-item-professional.active {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Professional Tables */
|
||||
.table-professional {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--light-surface);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px var(--light-shadow);
|
||||
}
|
||||
|
||||
.dark .table-professional {
|
||||
background: var(--dark-surface);
|
||||
box-shadow: 0 4px 20px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.table-professional th {
|
||||
background: var(--light-bg-secondary);
|
||||
color: var(--light-text-primary);
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--light-border);
|
||||
}
|
||||
|
||||
.dark .table-professional th {
|
||||
background: var(--dark-bg-secondary);
|
||||
color: var(--dark-text-primary);
|
||||
border-bottom-color: var(--dark-border);
|
||||
}
|
||||
|
||||
.table-professional td {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--light-border);
|
||||
color: var(--light-text-secondary);
|
||||
}
|
||||
|
||||
.dark .table-professional td {
|
||||
border-bottom-color: var(--dark-border);
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
.table-professional tbody tr:hover {
|
||||
background: var(--light-surface-hover);
|
||||
}
|
||||
|
||||
.dark .table-professional tbody tr:hover {
|
||||
background: var(--dark-surface-hover);
|
||||
}
|
||||
|
||||
/* Professional Alerts */
|
||||
.alert-professional {
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid transparent;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 4px 15px var(--light-shadow);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.dark .alert-info {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.dark .alert-success {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.dark .alert-warning {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.dark .alert-error {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Background Gradients für verschiedene Seiten */
|
||||
.bg-professional {
|
||||
background: linear-gradient(135deg, var(--light-bg-primary) 0%, var(--light-bg-secondary) 50%, var(--light-bg-tertiary) 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dark .bg-professional {
|
||||
background: linear-gradient(135deg, var(--dark-bg-primary) 0%, var(--dark-bg-secondary) 50%, var(--dark-bg-tertiary) 100%);
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-professional-primary {
|
||||
color: var(--light-text-primary);
|
||||
}
|
||||
|
||||
.dark .text-professional-primary {
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
.text-professional-secondary {
|
||||
color: var(--light-text-secondary);
|
||||
}
|
||||
|
||||
.dark .text-professional-secondary {
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
.text-professional-muted {
|
||||
color: var(--light-text-muted);
|
||||
}
|
||||
|
||||
.dark .text-professional-muted {
|
||||
color: var(--dark-text-muted);
|
||||
}
|
||||
|
||||
/* Smooth transitions für alle professionellen Komponenten */
|
||||
.professional-hero,
|
||||
.professional-container,
|
||||
.mb-glass,
|
||||
.btn-professional,
|
||||
.btn-secondary-professional,
|
||||
.input-professional,
|
||||
.card-professional,
|
||||
.stat-card,
|
||||
.status-professional,
|
||||
.nav-professional,
|
||||
.nav-item-professional,
|
||||
.table-professional,
|
||||
.alert-professional {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.professional-hero {
|
||||
margin: 1rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.card-professional {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.btn-professional,
|
||||
.btn-secondary-professional {
|
||||
padding: 0.625rem 1.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation-Klassen */
|
||||
.animate-fade-in {
|
||||
animation: fadeInProfessional 0.6s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUpProfessional 0.6s ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scaleInProfessional 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInProfessional {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUpProfessional {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleInProfessional {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
@ -24,6 +24,8 @@
|
||||
|
||||
<!-- CSS -->
|
||||
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/components.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/professional-theme.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Preload critical resources -->
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/ui-components.js') }}" as="script">
|
||||
|
365
backend/app/templates/guest_start_job.html
Normal file
365
backend/app/templates/guest_start_job.html
Normal file
@ -0,0 +1,365 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Job mit Code starten{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
||||
|
||||
<!-- Header Section -->
|
||||
<div class="relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-blue-600/10 to-purple-600/10"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
|
||||
<h1 class="text-4xl lg:text-6xl font-bold text-slate-900 dark:text-white mb-6">
|
||||
<span class="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
Job starten
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl text-slate-600 dark:text-slate-400 max-w-3xl mx-auto leading-relaxed">
|
||||
Geben Sie Ihren 6-stelligen Zugangscode ein, um Ihren genehmigten Druckauftrag zu starten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10">
|
||||
|
||||
<!-- Code-Eingabe Container -->
|
||||
<div class="form-container professional-shadow p-8 lg:p-12">
|
||||
<div class="text-center mb-10">
|
||||
<div class="w-20 h-20 bg-gradient-to-r from-green-500 to-emerald-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-slate-900 dark:text-white mb-4">
|
||||
Zugangscode eingeben
|
||||
</h2>
|
||||
<p class="text-lg text-slate-600 dark:text-slate-400">
|
||||
Ihr persönlicher 6-stelliger Code wurde Ihnen nach der Genehmigung mitgeteilt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Erfolgs-Nachricht (versteckt) -->
|
||||
<div id="successMessage" class="hidden mb-8 p-6 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border border-green-200 dark:border-green-700 rounded-2xl">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center mr-4">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-green-800 dark:text-green-200">Job erfolgreich gestartet!</h3>
|
||||
<p class="text-green-700 dark:text-green-300" id="successDetails"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fehler-Nachricht (versteckt) -->
|
||||
<div id="errorMessage" class="hidden mb-8 p-6 bg-gradient-to-r from-red-50 to-rose-50 dark:from-red-900/20 dark:to-rose-900/20 border border-red-200 dark:border-red-700 rounded-2xl">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center mr-4">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-red-800 dark:text-red-200">Fehler beim Starten</h3>
|
||||
<p class="text-red-700 dark:text-red-300" id="errorDetails"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="codeForm" class="space-y-8">
|
||||
|
||||
<!-- Code-Eingabe -->
|
||||
<div class="group">
|
||||
<label for="accessCode" class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-4">
|
||||
6-stelliger Zugangscode <span class="text-red-500">*</span>
|
||||
</label>
|
||||
|
||||
<!-- Code-Input mit einzelnen Feldern -->
|
||||
<div class="flex justify-center space-x-3 mb-4">
|
||||
<input type="text" id="code1" maxlength="1"
|
||||
class="code-input w-16 h-16 text-center text-2xl font-bold border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-4 focus:ring-blue-500/25 focus:border-blue-500 dark:bg-slate-800 dark:text-white"
|
||||
oninput="moveToNext(this, 'code2')" onkeydown="handleBackspace(event, this, null)">
|
||||
<input type="text" id="code2" maxlength="1"
|
||||
class="code-input w-16 h-16 text-center text-2xl font-bold border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-4 focus:ring-blue-500/25 focus:border-blue-500 dark:bg-slate-800 dark:text-white"
|
||||
oninput="moveToNext(this, 'code3')" onkeydown="handleBackspace(event, this, 'code1')">
|
||||
<input type="text" id="code3" maxlength="1"
|
||||
class="code-input w-16 h-16 text-center text-2xl font-bold border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-4 focus:ring-blue-500/25 focus:border-blue-500 dark:bg-slate-800 dark:text-white"
|
||||
oninput="moveToNext(this, 'code4')" onkeydown="handleBackspace(event, this, 'code2')">
|
||||
<input type="text" id="code4" maxlength="1"
|
||||
class="code-input w-16 h-16 text-center text-2xl font-bold border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-4 focus:ring-blue-500/25 focus:border-blue-500 dark:bg-slate-800 dark:text-white"
|
||||
oninput="moveToNext(this, 'code5')" onkeydown="handleBackspace(event, this, 'code3')">
|
||||
<input type="text" id="code5" maxlength="1"
|
||||
class="code-input w-16 h-16 text-center text-2xl font-bold border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-4 focus:ring-blue-500/25 focus:border-blue-500 dark:bg-slate-800 dark:text-white"
|
||||
oninput="moveToNext(this, 'code6')" onkeydown="handleBackspace(event, this, 'code4')">
|
||||
<input type="text" id="code6" maxlength="1"
|
||||
class="code-input w-16 h-16 text-center text-2xl font-bold border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-4 focus:ring-blue-500/25 focus:border-blue-500 dark:bg-slate-800 dark:text-white"
|
||||
oninput="moveToNext(this, null)" onkeydown="handleBackspace(event, this, 'code5')">
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
Der Code besteht aus 6 Zeichen (Großbuchstaben und Zahlen)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="text-center pt-6">
|
||||
<button type="submit" id="submitBtn"
|
||||
class="w-full sm:w-auto px-12 py-4 bg-gradient-to-r from-blue-600 to-indigo-600 text-white font-semibold rounded-2xl hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-4 focus:ring-blue-500/25 transform transition-all duration-300 hover:scale-105 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none">
|
||||
<span class="flex items-center justify-center">
|
||||
<svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M12 5v.01M12 19v.01M12 12h.01M12 9a3 3 0 100-6 3 3 0 000 6zm0 0a3 3 0 100 6 3 3 0 000-6z"/>
|
||||
</svg>
|
||||
Job jetzt starten
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Hilfe-Sektion -->
|
||||
<div class="mt-12 pt-8 border-t border-slate-200 dark:border-slate-700">
|
||||
<div class="text-center">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">
|
||||
Brauchen Sie Hilfe?
|
||||
</h3>
|
||||
<div class="space-y-3 text-sm text-slate-600 dark:text-slate-400">
|
||||
<p>• Ihr Zugangscode wurde Ihnen nach der Genehmigung mitgeteilt</p>
|
||||
<p>• Der Code ist nur einmalig verwendbar und hat eine begrenzte Gültigkeit</p>
|
||||
<p>• Bei Problemen wenden Sie sich an den Administrator</p>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<a href="{{ url_for('guest.guest_request_form') }}"
|
||||
class="inline-flex items-center text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
Neue Anfrage stellen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.dark .form-container {
|
||||
background: rgba(30, 41, 59, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.professional-shadow {
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.code-input {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.code-input:focus {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.code-input.valid {
|
||||
border-color: #10b981;
|
||||
background-color: #ecfdf5;
|
||||
}
|
||||
|
||||
.dark .code-input.valid {
|
||||
border-color: #059669;
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Code-Eingabe-Logik
|
||||
function moveToNext(current, nextId) {
|
||||
const value = current.value.toUpperCase();
|
||||
|
||||
// Nur alphanumerische Zeichen erlauben
|
||||
if (!/^[A-Z0-9]$/.test(value)) {
|
||||
current.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
current.value = value;
|
||||
current.classList.add('valid');
|
||||
|
||||
// Zum nächsten Feld wechseln
|
||||
if (nextId && value) {
|
||||
document.getElementById(nextId).focus();
|
||||
}
|
||||
|
||||
// Prüfen ob alle Felder ausgefüllt sind
|
||||
checkFormComplete();
|
||||
}
|
||||
|
||||
function handleBackspace(event, current, prevId) {
|
||||
if (event.key === 'Backspace') {
|
||||
if (current.value === '' && prevId) {
|
||||
event.preventDefault();
|
||||
const prevField = document.getElementById(prevId);
|
||||
prevField.focus();
|
||||
prevField.value = '';
|
||||
prevField.classList.remove('valid');
|
||||
} else if (current.value !== '') {
|
||||
current.classList.remove('valid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkFormComplete() {
|
||||
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
|
||||
const allFilled = inputs.every(id => document.getElementById(id).value !== '');
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = !allFilled;
|
||||
|
||||
if (allFilled) {
|
||||
submitBtn.classList.add('ring-4', 'ring-green-500/25');
|
||||
} else {
|
||||
submitBtn.classList.remove('ring-4', 'ring-green-500/25');
|
||||
}
|
||||
}
|
||||
|
||||
function getCodeValue() {
|
||||
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
|
||||
return inputs.map(id => document.getElementById(id).value).join('');
|
||||
}
|
||||
|
||||
function clearCode() {
|
||||
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
|
||||
inputs.forEach(id => {
|
||||
const input = document.getElementById(id);
|
||||
input.value = '';
|
||||
input.classList.remove('valid');
|
||||
});
|
||||
checkFormComplete();
|
||||
document.getElementById('code1').focus();
|
||||
}
|
||||
|
||||
function showSuccess(message, details) {
|
||||
document.getElementById('successMessage').classList.remove('hidden');
|
||||
document.getElementById('successDetails').textContent = details;
|
||||
document.getElementById('errorMessage').classList.add('hidden');
|
||||
|
||||
// Scroll zur Nachricht
|
||||
document.getElementById('successMessage').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
function showError(message, details) {
|
||||
document.getElementById('errorMessage').classList.remove('hidden');
|
||||
document.getElementById('errorDetails').textContent = details;
|
||||
document.getElementById('successMessage').classList.add('hidden');
|
||||
|
||||
// Scroll zur Nachricht
|
||||
document.getElementById('errorMessage').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
// Form-Submit-Handler
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('codeForm');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
// Erstes Feld fokussieren
|
||||
document.getElementById('code1').focus();
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const code = getCodeValue();
|
||||
if (code.length !== 6) {
|
||||
showError('Ungültiger Code', 'Bitte geben Sie alle 6 Zeichen ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Button-Animation
|
||||
submitBtn.disabled = true;
|
||||
const originalContent = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = `
|
||||
<span class="flex items-center justify-center">
|
||||
<svg class="animate-spin w-5 h-5 mr-3" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Wird überprüft...
|
||||
</span>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/guest/start-job', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({ code: code })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(
|
||||
'Job erfolgreich gestartet!',
|
||||
`Ihr Job "${result.job_name}" wurde gestartet und läuft bis ${result.end_time}.`
|
||||
);
|
||||
|
||||
// Form deaktivieren
|
||||
form.style.opacity = '0.5';
|
||||
form.style.pointerEvents = 'none';
|
||||
|
||||
// Nach 3 Sekunden zur Job-Status-Seite weiterleiten
|
||||
setTimeout(() => {
|
||||
if (result.job_id) {
|
||||
window.location.href = `/guest/job/${result.job_id}/status`;
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
} else {
|
||||
showError('Code ungültig', result.error || 'Der eingegebene Code ist ungültig oder bereits verwendet.');
|
||||
clearCode();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showError('Verbindungsfehler', 'Es gab ein Problem bei der Verbindung. Bitte versuchen Sie es erneut.');
|
||||
clearCode();
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalContent;
|
||||
checkFormComplete();
|
||||
}
|
||||
});
|
||||
|
||||
// Paste-Handler für kompletten Code
|
||||
document.addEventListener('paste', function(e) {
|
||||
const target = e.target;
|
||||
if (target.classList.contains('code-input')) {
|
||||
e.preventDefault();
|
||||
const paste = (e.clipboardData || window.clipboardData).getData('text').toUpperCase();
|
||||
|
||||
if (paste.length === 6 && /^[A-Z0-9]+$/.test(paste)) {
|
||||
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
|
||||
inputs.forEach((id, index) => {
|
||||
const input = document.getElementById(id);
|
||||
input.value = paste[index] || '';
|
||||
if (input.value) input.classList.add('valid');
|
||||
});
|
||||
checkFormComplete();
|
||||
document.getElementById('code6').focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user