🔧 Update: Enhance Guest Request Management with OTP and Name Verification
**Änderungen:** - ✅ Hinzugefügt: Neue Methode `find_by_otp_and_name` in `GuestRequest`, um Gastanfragen anhand von OTP-Code und Name zu finden. - ✅ API-Endpunkte in `admin_unified.py` für die Verwaltung von Gastanfragen mit OTP-Codes implementiert, einschließlich Generierung und Druck von Zugangsdaten. - ✅ Anpassungen in `guest.py`, um die Authentifizierung von Gastanfragen mit Name und OTP-Code zu unterstützen. **Ergebnis:** - Verbesserte Sicherheit und Benutzerfreundlichkeit bei der Verwaltung von Gastanfragen im Offline-System. - Klarere API-Responses und verbesserte Fehlerbehandlung für Gastanfragen. 🤖 Generated with [Claude Code](https://claude.ai/code)
This commit is contained in:
@ -26,7 +26,7 @@ from datetime import datetime, timedelta
|
|||||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, current_app
|
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, current_app
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from models import User, Printer, Job, get_cached_session, Stats, SystemLog, PlugStatusLog
|
from models import User, Printer, Job, get_cached_session, Stats, SystemLog, PlugStatusLog, GuestRequest
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
# ===== BLUEPRINT-KONFIGURATION =====
|
# ===== BLUEPRINT-KONFIGURATION =====
|
||||||
@ -1283,6 +1283,214 @@ def export_logs_api():
|
|||||||
admin_logger.error(f"Fehler beim Exportieren der Logs: {str(e)}")
|
admin_logger.error(f"Fehler beim Exportieren der Logs: {str(e)}")
|
||||||
return jsonify({"error": "Fehler beim Exportieren der Logs"}), 500
|
return jsonify({"error": "Fehler beim Exportieren der Logs"}), 500
|
||||||
|
|
||||||
|
# ===== GAST-OTP-MANAGEMENT FÜR OFFLINE-BETRIEB =====
|
||||||
|
|
||||||
|
@admin_api_blueprint.route("/guest-requests", methods=["GET"])
|
||||||
|
@admin_required
|
||||||
|
def get_guest_requests_api():
|
||||||
|
"""API-Endpunkt zum Abrufen aller Gastanfragen mit OTP-Codes für Admins"""
|
||||||
|
try:
|
||||||
|
with get_cached_session() as db_session:
|
||||||
|
# Alle Gastanfragen laden
|
||||||
|
guest_requests = db_session.query(GuestRequest).order_by(
|
||||||
|
GuestRequest.created_at.desc()
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# In Dictionary konvertieren mit OTP-Codes für Admins
|
||||||
|
requests_data = []
|
||||||
|
for req in guest_requests:
|
||||||
|
request_data = {
|
||||||
|
'id': req.id,
|
||||||
|
'name': req.name,
|
||||||
|
'email': req.email,
|
||||||
|
'reason': req.reason,
|
||||||
|
'status': req.status,
|
||||||
|
'duration_min': req.duration_min,
|
||||||
|
'created_at': req.created_at.isoformat() if req.created_at else None,
|
||||||
|
'processed_at': req.processed_at.isoformat() if req.processed_at else None,
|
||||||
|
'processed_by': req.processed_by,
|
||||||
|
'approval_notes': req.approval_notes,
|
||||||
|
'rejection_reason': req.rejection_reason,
|
||||||
|
'author_ip': req.author_ip
|
||||||
|
}
|
||||||
|
|
||||||
|
# OTP-Code für Admins sichtbar machen (nur wenn aktiv)
|
||||||
|
if req.status == 'approved' and req.otp_code and req.otp_expires_at:
|
||||||
|
if req.otp_expires_at > datetime.now() and not req.otp_used_at:
|
||||||
|
request_data['otp_code'] = req.otp_code # Klartext für Admin
|
||||||
|
request_data['otp_expires_at'] = req.otp_expires_at.isoformat()
|
||||||
|
request_data['otp_status'] = 'active'
|
||||||
|
elif req.otp_used_at:
|
||||||
|
request_data['otp_status'] = 'used'
|
||||||
|
request_data['otp_used_at'] = req.otp_used_at.isoformat()
|
||||||
|
else:
|
||||||
|
request_data['otp_status'] = 'expired'
|
||||||
|
else:
|
||||||
|
request_data['otp_status'] = 'not_generated'
|
||||||
|
|
||||||
|
requests_data.append(request_data)
|
||||||
|
|
||||||
|
admin_logger.info(f"Gastanfragen abgerufen: {len(requests_data)} Einträge für Admin {current_user.name}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"requests": requests_data,
|
||||||
|
"count": len(requests_data)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
admin_logger.error(f"Fehler beim Abrufen der Gastanfragen: {str(e)}")
|
||||||
|
return jsonify({"error": "Fehler beim Laden der Gastanfragen"}), 500
|
||||||
|
|
||||||
|
@admin_api_blueprint.route("/guest-requests/<int:request_id>/generate-otp", methods=["POST"])
|
||||||
|
@admin_required
|
||||||
|
def generate_guest_otp_api(request_id):
|
||||||
|
"""Generiert einen neuen OTP-Code für eine genehmigte Gastanfrage"""
|
||||||
|
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": "Gastanfrage nicht gefunden"}), 404
|
||||||
|
|
||||||
|
if guest_request.status != 'approved':
|
||||||
|
return jsonify({"error": "Gastanfrage muss erst genehmigt werden"}), 400
|
||||||
|
|
||||||
|
# Neuen OTP-Code generieren
|
||||||
|
otp_code = guest_request.generate_otp()
|
||||||
|
guest_request.otp_expires_at = datetime.now() + timedelta(hours=72) # 72h gültig
|
||||||
|
guest_request.otp_used_at = None # Reset falls bereits verwendet
|
||||||
|
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
admin_logger.info(f"Neuer OTP-Code generiert für Gastanfrage {request_id} von Admin {current_user.name}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "Neuer OTP-Code generiert",
|
||||||
|
"otp_code": otp_code,
|
||||||
|
"expires_at": guest_request.otp_expires_at.isoformat(),
|
||||||
|
"guest_name": guest_request.name
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
admin_logger.error(f"Fehler beim Generieren des OTP-Codes: {str(e)}")
|
||||||
|
return jsonify({"error": "Fehler beim Generieren des OTP-Codes"}), 500
|
||||||
|
|
||||||
|
@admin_api_blueprint.route("/guest-requests/<int:request_id>/print-credentials", methods=["POST"])
|
||||||
|
@admin_required
|
||||||
|
def print_guest_credentials_api(request_id):
|
||||||
|
"""Erstellt Ausdruck-Template für Gast-Zugangsdaten"""
|
||||||
|
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": "Gastanfrage nicht gefunden"}), 404
|
||||||
|
|
||||||
|
if guest_request.status != 'approved':
|
||||||
|
return jsonify({"error": "Gastanfrage muss erst genehmigt werden"}), 400
|
||||||
|
|
||||||
|
if not guest_request.otp_code or not guest_request.otp_expires_at:
|
||||||
|
return jsonify({"error": "Kein OTP-Code verfügbar"}), 400
|
||||||
|
|
||||||
|
# Ausdruck-Template erstellen
|
||||||
|
print_template = {
|
||||||
|
"type": "guest_credentials",
|
||||||
|
"title": "MYP GASTZUGANG GENEHMIGT",
|
||||||
|
"subtitle": "TBA Marienfelde - Offline System",
|
||||||
|
"guest_info": {
|
||||||
|
"name": guest_request.name,
|
||||||
|
"request_id": f"GAS-{guest_request.id:06d}",
|
||||||
|
"email": guest_request.email,
|
||||||
|
"approved_at": guest_request.processed_at.strftime("%d.%m.%Y %H:%M") if guest_request.processed_at else None,
|
||||||
|
"approved_by": guest_request.processed_by
|
||||||
|
},
|
||||||
|
"access_data": {
|
||||||
|
"otp_code": guest_request.otp_code,
|
||||||
|
"valid_until": guest_request.otp_expires_at.strftime("%d.%m.%Y %H:%M"),
|
||||||
|
"login_url": "http://192.168.1.100:5000/auth/guest"
|
||||||
|
},
|
||||||
|
"usage_rules": [
|
||||||
|
"Max. Druckzeit pro Job: 4 Stunden",
|
||||||
|
"Dateiformate: STL, OBJ, 3MF, GCODE",
|
||||||
|
"Materialien: PLA, PETG",
|
||||||
|
"Jobs benötigen Admin-Freigabe"
|
||||||
|
],
|
||||||
|
"pickup_info": {
|
||||||
|
"location": "TBA Marienfelde, Raum B2.1",
|
||||||
|
"hours": "Mo-Fr 8:00-16:00",
|
||||||
|
"storage_days": "Max. 7 Tage"
|
||||||
|
},
|
||||||
|
"qr_code_data": f"http://192.168.1.100:5000/auth/guest?name={guest_request.name}&id={guest_request.id}",
|
||||||
|
"admin_note": "An Gast aushändigen",
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
admin_logger.info(f"Ausdruck-Template erstellt für Gastanfrage {request_id} von Admin {current_user.name}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"print_template": print_template
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
admin_logger.error(f"Fehler beim Erstellen des Ausdruck-Templates: {str(e)}")
|
||||||
|
return jsonify({"error": "Fehler beim Erstellen des Ausdruck-Templates"}), 500
|
||||||
|
|
||||||
|
@admin_api_blueprint.route("/guest-requests/pending-otps", methods=["GET"])
|
||||||
|
@admin_required
|
||||||
|
def get_pending_guest_otps_api():
|
||||||
|
"""Listet alle aktiven OTP-Codes für schnelle Admin-Übersicht"""
|
||||||
|
try:
|
||||||
|
with get_cached_session() as db_session:
|
||||||
|
# Alle genehmigten Anfragen mit aktiven OTP-Codes
|
||||||
|
active_requests = db_session.query(GuestRequest).filter(
|
||||||
|
GuestRequest.status == 'approved',
|
||||||
|
GuestRequest.otp_code.isnot(None),
|
||||||
|
GuestRequest.otp_expires_at > datetime.now(),
|
||||||
|
GuestRequest.otp_used_at.is_(None)
|
||||||
|
).order_by(GuestRequest.otp_expires_at.asc()).all()
|
||||||
|
|
||||||
|
# Kompakte Liste für Admin-Dashboard
|
||||||
|
otps_data = []
|
||||||
|
for req in active_requests:
|
||||||
|
time_remaining = req.otp_expires_at - datetime.now()
|
||||||
|
hours_remaining = int(time_remaining.total_seconds() // 3600)
|
||||||
|
|
||||||
|
otps_data.append({
|
||||||
|
'request_id': req.id,
|
||||||
|
'guest_name': req.name,
|
||||||
|
'otp_code': req.otp_code,
|
||||||
|
'expires_at': req.otp_expires_at.isoformat(),
|
||||||
|
'hours_remaining': hours_remaining,
|
||||||
|
'urgency': 'critical' if hours_remaining < 2 else 'warning' if hours_remaining < 24 else 'normal'
|
||||||
|
})
|
||||||
|
|
||||||
|
admin_logger.info(f"Aktive OTP-Codes abgerufen: {len(otps_data)} Codes")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"active_otps": otps_data,
|
||||||
|
"count": len(otps_data)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
admin_logger.error(f"Fehler beim Abrufen aktiver OTP-Codes: {str(e)}")
|
||||||
|
return jsonify({"error": "Fehler beim Laden der OTP-Codes"}), 500
|
||||||
|
|
||||||
|
# ===== ADMIN-UI ROUTES FÜR GAST-OTP-VERWALTUNG =====
|
||||||
|
|
||||||
|
@admin_blueprint.route("/guest-otps")
|
||||||
|
@admin_required
|
||||||
|
def guest_otps_management():
|
||||||
|
"""Admin-UI für Gast-OTP-Verwaltung (Offline-System)"""
|
||||||
|
admin_logger.info(f"Gast-OTP-Verwaltung aufgerufen von Admin {current_user.name}")
|
||||||
|
|
||||||
|
return render_template('admin_guest_otps.html',
|
||||||
|
page_title="Gast-OTP-Verwaltung",
|
||||||
|
current_user=current_user)
|
||||||
|
|
||||||
# ===== API-ENDPUNKTE FÜR SYSTEM-INFORMATIONEN =====
|
# ===== API-ENDPUNKTE FÜR SYSTEM-INFORMATIONEN =====
|
||||||
|
|
||||||
@admin_api_blueprint.route("/system/status", methods=["GET"])
|
@admin_api_blueprint.route("/system/status", methods=["GET"])
|
||||||
|
@ -380,24 +380,29 @@ def api_create_guest_request():
|
|||||||
@guest_blueprint.route('/api/guest/start-job', methods=['POST'])
|
@guest_blueprint.route('/api/guest/start-job', methods=['POST'])
|
||||||
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
|
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
|
||||||
def api_start_job_with_code():
|
def api_start_job_with_code():
|
||||||
"""Job mit 6-stelligem OTP-Code starten."""
|
"""Job mit Name + 6-stelligem OTP-Code starten (Offline-System)."""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data or 'code' not in data:
|
if not data or 'code' not in data or 'name' not in data:
|
||||||
return jsonify({"error": "Code ist erforderlich"}), 400
|
return jsonify({"error": "Name und Code sind erforderlich"}), 400
|
||||||
|
|
||||||
code = data['code'].strip().upper()
|
code = data['code'].strip().upper()
|
||||||
|
name = data['name'].strip()
|
||||||
|
|
||||||
if len(code) != 6:
|
if len(code) != 6:
|
||||||
return jsonify({"error": "Code muss 6 Zeichen lang sein"}), 400
|
return jsonify({"error": "Code muss 6 Zeichen lang sein"}), 400
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return jsonify({"error": "Name ist erforderlich"}), 400
|
||||||
|
|
||||||
with get_cached_session() as db_session:
|
with get_cached_session() as db_session:
|
||||||
# Gastanfrage anhand des OTP-Codes finden
|
# Gastanfrage anhand des OTP-Codes UND Names finden
|
||||||
matching_request = GuestRequest.find_by_otp(code)
|
matching_request = GuestRequest.find_by_otp_and_name(code, name)
|
||||||
|
|
||||||
if not matching_request:
|
if not matching_request:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "Ungültiger oder bereits verwendeter Code"
|
"error": "Ungültiger Code oder Name stimmt nicht überein"
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# Prüfen ob zugehöriger Job existiert
|
# Prüfen ob zugehöriger Job existiert
|
||||||
@ -997,8 +1002,8 @@ def api_get_request_otp(request_id):
|
|||||||
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
|
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
|
||||||
def api_guest_status_by_otp():
|
def api_guest_status_by_otp():
|
||||||
"""
|
"""
|
||||||
Öffentliche Route für Gäste um ihren Auftragsstatus mit OTP-Code zu prüfen.
|
Öffentliche Route für Gäste um ihren Auftragsstatus mit Name + OTP-Code zu prüfen.
|
||||||
Keine Authentifizierung erforderlich.
|
Keine Authentifizierung erforderlich (Offline-System).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -1009,7 +1014,7 @@ def api_guest_status_by_otp():
|
|||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
otp_code = data.get('otp_code', '').strip()
|
otp_code = data.get('otp_code', '').strip()
|
||||||
email = data.get('email', '').strip() # Optional für zusätzliche Verifikation
|
name = data.get('name', '').strip()
|
||||||
|
|
||||||
if not otp_code:
|
if not otp_code:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -1017,26 +1022,21 @@ def api_guest_status_by_otp():
|
|||||||
'message': 'OTP-Code ist erforderlich'
|
'message': 'OTP-Code ist erforderlich'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
with get_cached_session() as db_session:
|
if not name:
|
||||||
# Alle Gastaufträge mit OTP-Codes finden
|
|
||||||
guest_requests = db_session.query(GuestRequest).filter(
|
|
||||||
GuestRequest.otp_code.isnot(None)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
found_request = None
|
|
||||||
for request_obj in guest_requests:
|
|
||||||
if request_obj.verify_otp(otp_code):
|
|
||||||
# Zusätzliche E-Mail-Verifikation falls angegeben
|
|
||||||
if email and request_obj.email and request_obj.email.lower() != email.lower():
|
|
||||||
continue
|
|
||||||
found_request = request_obj
|
|
||||||
break
|
|
||||||
|
|
||||||
if not found_request:
|
|
||||||
logger.warning(f"Ungültiger OTP-Code für Gast-Status-Abfrage: {otp_code[:4]}****")
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': 'Ungültiger Code oder E-Mail-Adresse'
|
'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
|
}), 404
|
||||||
|
|
||||||
# Status-Informationen für den Gast zusammenstellen
|
# Status-Informationen für den Gast zusammenstellen
|
||||||
|
@ -1229,6 +1229,41 @@ class GuestRequest(Base):
|
|||||||
logger.error(f"Fehler beim Suchen der Gastanfrage per OTP: {str(e)}")
|
logger.error(f"Fehler beim Suchen der Gastanfrage per OTP: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_by_otp_and_name(cls, otp_code: str, name: str) -> Optional['GuestRequest']:
|
||||||
|
"""
|
||||||
|
Findet eine Gastanfrage anhand des OTP-Codes UND Names (für Offline-System).
|
||||||
|
Zusätzliche Sicherheit durch Name-Verifikation.
|
||||||
|
"""
|
||||||
|
if not otp_code or len(otp_code) != 6 or not name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with get_cached_session() as session:
|
||||||
|
# Alle genehmigten Gastanfragen mit OTP-Codes und passendem Namen finden
|
||||||
|
guest_requests = session.query(cls).filter(
|
||||||
|
cls.status == "approved",
|
||||||
|
cls.otp_code.isnot(None),
|
||||||
|
cls.otp_used_at.is_(None), # Noch nicht verwendet
|
||||||
|
cls.name.ilike(f"%{name.strip()}%") # Name-Matching (case-insensitive)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Code gegen alle passenden Anfragen prüfen
|
||||||
|
for request in guest_requests:
|
||||||
|
if request.verify_otp(otp_code):
|
||||||
|
# Zusätzliche Name-Verifikation (exakte Übereinstimmung)
|
||||||
|
if request.name.strip().lower() == name.strip().lower():
|
||||||
|
logger.info(f"Gastanfrage {request.id} erfolgreich per Name+OTP authentifiziert")
|
||||||
|
return request
|
||||||
|
else:
|
||||||
|
logger.warning(f"OTP stimmt, aber Name passt nicht exakt: '{request.name}' vs '{name}'")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Suchen der Gastanfrage per Name+OTP: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class JobOrder(Base):
|
class JobOrder(Base):
|
||||||
"""
|
"""
|
||||||
|
574
backend/templates/admin_guest_otps.html
Normal file
574
backend/templates/admin_guest_otps.html
Normal file
@ -0,0 +1,574 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Gast-OTP-Verwaltung - Mercedes-Benz TBA Marienfelde{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<style>
|
||||||
|
.otp-card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: 4px solid #10b981;
|
||||||
|
}
|
||||||
|
.otp-card.critical {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
.otp-card.warning {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.print-template {
|
||||||
|
font-family: monospace;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.02); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-6 py-8">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
🔑 Gast-OTP-Verwaltung
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mt-2">
|
||||||
|
Offline-System - OTP-Codes für Gastbenutzer verwalten
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<button onclick="loadGuestRequests()"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
|
🔄 Aktualisieren
|
||||||
|
</button>
|
||||||
|
<button onclick="showActiveOTPs()"
|
||||||
|
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||||
|
📊 Aktive OTPs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 bg-blue-100 dark:bg-blue-900 rounded-full">
|
||||||
|
<span class="text-2xl">📝</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Ausstehend</p>
|
||||||
|
<p id="pending-count" class="text-2xl font-bold text-gray-900 dark:text-white">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 bg-green-100 dark:bg-green-900 rounded-full">
|
||||||
|
<span class="text-2xl">✅</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Aktive OTPs</p>
|
||||||
|
<p id="active-otps-count" class="text-2xl font-bold text-gray-900 dark:text-white">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 bg-red-100 dark:bg-red-900 rounded-full">
|
||||||
|
<span class="text-2xl">⚠️</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Kritisch</p>
|
||||||
|
<p id="critical-count" class="text-2xl font-bold text-gray-900 dark:text-white">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 bg-gray-100 dark:bg-gray-700 rounded-full">
|
||||||
|
<span class="text-2xl">📋</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Gesamt</p>
|
||||||
|
<p id="total-requests-count" class="text-2xl font-bold text-gray-900 dark:text-white">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active OTPs Panel -->
|
||||||
|
<div id="active-otps-panel" class="mb-8 hidden">
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
⚡ Aktive OTP-Codes
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Sofort verfügbare Codes für Gastbenutzer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="active-otps-list" class="p-6">
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Guest Requests List -->
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
👥 Gastanfragen
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Alle Anfragen mit OTP-Management
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="guest-requests-list" class="p-6">
|
||||||
|
<div class="flex items-center justify-center py-12">
|
||||||
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||||
|
<span class="ml-4 text-gray-600 dark:text-gray-400">Lade Gastanfragen...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Modal -->
|
||||||
|
<div id="print-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden">
|
||||||
|
<div class="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
🖨️ Ausdruck-Vorlage
|
||||||
|
</h3>
|
||||||
|
<button onclick="closePrintModal()"
|
||||||
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="print-content" class="p-6">
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-4">
|
||||||
|
<button onclick="closePrintModal()"
|
||||||
|
class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button onclick="printTemplate()"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||||
|
🖨️ Drucken
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Global state
|
||||||
|
let guestRequests = [];
|
||||||
|
let activeOTPs = [];
|
||||||
|
|
||||||
|
// CSRF Token
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||||
|
|
||||||
|
// Initialize page
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadGuestRequests();
|
||||||
|
loadActiveOTPs();
|
||||||
|
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(() => {
|
||||||
|
loadGuestRequests();
|
||||||
|
loadActiveOTPs();
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load guest requests
|
||||||
|
async function loadGuestRequests() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/guest-requests', {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
guestRequests = data.requests;
|
||||||
|
updateStats();
|
||||||
|
renderGuestRequests();
|
||||||
|
} else {
|
||||||
|
showError('Fehler beim Laden der Gastanfragen');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading guest requests:', error);
|
||||||
|
showError('Verbindungsfehler beim Laden der Gastanfragen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load active OTPs
|
||||||
|
async function loadActiveOTPs() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/guest-requests/pending-otps', {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
activeOTPs = data.active_otps;
|
||||||
|
renderActiveOTPs();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading active OTPs:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
function updateStats() {
|
||||||
|
const pending = guestRequests.filter(req => req.status === 'pending').length;
|
||||||
|
const activeOTPsCount = guestRequests.filter(req => req.otp_status === 'active').length;
|
||||||
|
const critical = activeOTPs.filter(otp => otp.urgency === 'critical').length;
|
||||||
|
|
||||||
|
document.getElementById('pending-count').textContent = pending;
|
||||||
|
document.getElementById('active-otps-count').textContent = activeOTPsCount;
|
||||||
|
document.getElementById('critical-count').textContent = critical;
|
||||||
|
document.getElementById('total-requests-count').textContent = guestRequests.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render guest requests
|
||||||
|
function renderGuestRequests() {
|
||||||
|
const container = document.getElementById('guest-requests-list');
|
||||||
|
|
||||||
|
if (guestRequests.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<div class="text-6xl mb-4">📝</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Keine Gastanfragen
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
Es sind aktuell keine Gastanfragen vorhanden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = guestRequests.map(request => `
|
||||||
|
<div class="otp-card bg-gray-50 dark:bg-slate-700 rounded-lg p-6 mb-4 ${getUrgencyClass(request)}">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center space-x-4 mb-3">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
${request.name}
|
||||||
|
</h3>
|
||||||
|
<span class="px-3 py-1 rounded-full text-sm font-medium ${getStatusBadgeClass(request.status)}">
|
||||||
|
${getStatusText(request.status)}
|
||||||
|
</span>
|
||||||
|
${request.otp_status === 'active' ? `
|
||||||
|
<span class="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm font-medium">
|
||||||
|
OTP Aktiv
|
||||||
|
</span>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">E-Mail</p>
|
||||||
|
<p class="font-medium text-gray-900 dark:text-white">${request.email}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Erstellt</p>
|
||||||
|
<p class="font-medium text-gray-900 dark:text-white">
|
||||||
|
${request.created_at ? new Date(request.created_at).toLocaleString('de-DE') : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Dauer</p>
|
||||||
|
<p class="font-medium text-gray-900 dark:text-white">${request.duration_min} Min</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${request.reason ? `
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 text-sm">Grund</p>
|
||||||
|
<p class="text-gray-900 dark:text-white">${request.reason}</p>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${request.otp_code && request.status === 'approved' ? `
|
||||||
|
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-blue-900 dark:text-blue-300">OTP-Code</p>
|
||||||
|
<p class="text-2xl font-mono font-bold text-blue-900 dark:text-blue-300">
|
||||||
|
${request.otp_code}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||||
|
Gültig bis: ${request.otp_expires_at ? new Date(request.otp_expires_at).toLocaleString('de-DE') : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-2 ml-4">
|
||||||
|
${request.status === 'approved' ? `
|
||||||
|
<button onclick="generateNewOTP(${request.id})"
|
||||||
|
class="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm">
|
||||||
|
🔄 Neuer OTP
|
||||||
|
</button>
|
||||||
|
<button onclick="printCredentials(${request.id})"
|
||||||
|
class="px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm">
|
||||||
|
🖨️ Ausdruck
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render active OTPs
|
||||||
|
function renderActiveOTPs() {
|
||||||
|
const container = document.getElementById('active-otps-list');
|
||||||
|
|
||||||
|
if (activeOTPs.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<p class="text-center text-gray-600 dark:text-gray-400">
|
||||||
|
Keine aktiven OTP-Codes vorhanden.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = activeOTPs.map(otp => `
|
||||||
|
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-slate-700 rounded-lg mb-3">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="p-2 bg-blue-100 dark:bg-blue-900 rounded-full">
|
||||||
|
<span class="text-lg">🔑</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-gray-900 dark:text-white">${otp.guest_name}</p>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Läuft ab in ${otp.hours_remaining}h
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-xl font-mono font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
${otp.otp_code}
|
||||||
|
</p>
|
||||||
|
<span class="px-2 py-1 rounded text-xs font-medium ${getUrgencyBadgeClass(otp.urgency)}">
|
||||||
|
${getUrgencyText(otp.urgency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function getStatusText(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'pending': 'Ausstehend',
|
||||||
|
'approved': 'Genehmigt',
|
||||||
|
'rejected': 'Abgelehnt'
|
||||||
|
};
|
||||||
|
return statusMap[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusBadgeClass(status) {
|
||||||
|
const classMap = {
|
||||||
|
'pending': 'bg-yellow-100 text-yellow-800',
|
||||||
|
'approved': 'bg-green-100 text-green-800',
|
||||||
|
'rejected': 'bg-red-100 text-red-800'
|
||||||
|
};
|
||||||
|
return classMap[status] || 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUrgencyClass(request) {
|
||||||
|
if (request.otp_status === 'active') {
|
||||||
|
const hoursRemaining = calculateHoursRemaining(request.otp_expires_at);
|
||||||
|
if (hoursRemaining < 2) return 'critical';
|
||||||
|
if (hoursRemaining < 24) return 'warning';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUrgencyBadgeClass(urgency) {
|
||||||
|
const classMap = {
|
||||||
|
'critical': 'bg-red-100 text-red-800',
|
||||||
|
'warning': 'bg-yellow-100 text-yellow-800',
|
||||||
|
'normal': 'bg-green-100 text-green-800'
|
||||||
|
};
|
||||||
|
return classMap[urgency] || 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUrgencyText(urgency) {
|
||||||
|
const textMap = {
|
||||||
|
'critical': 'Kritisch',
|
||||||
|
'warning': 'Warnung',
|
||||||
|
'normal': 'Normal'
|
||||||
|
};
|
||||||
|
return textMap[urgency] || urgency;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateHoursRemaining(expiresAt) {
|
||||||
|
if (!expiresAt) return 0;
|
||||||
|
const now = new Date();
|
||||||
|
const expires = new Date(expiresAt);
|
||||||
|
const diffMs = expires.getTime() - now.getTime();
|
||||||
|
return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
async function generateNewOTP(requestId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/guest-requests/${requestId}/generate-otp`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showSuccess(`Neuer OTP-Code generiert: ${data.otp_code}`);
|
||||||
|
loadGuestRequests();
|
||||||
|
loadActiveOTPs();
|
||||||
|
} else {
|
||||||
|
showError(data.error || 'Fehler beim Generieren des OTP-Codes');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating OTP:', error);
|
||||||
|
showError('Verbindungsfehler beim Generieren des OTP-Codes');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printCredentials(requestId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/guest-requests/${requestId}/print-credentials`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showPrintModal(data.print_template);
|
||||||
|
} else {
|
||||||
|
showError(data.error || 'Fehler beim Erstellen des Ausdrucks');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating print template:', error);
|
||||||
|
showError('Verbindungsfehler beim Erstellen des Ausdrucks');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showActiveOTPs() {
|
||||||
|
const panel = document.getElementById('active-otps-panel');
|
||||||
|
panel.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPrintModal(template) {
|
||||||
|
const modal = document.getElementById('print-modal');
|
||||||
|
const content = document.getElementById('print-content');
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="print-template">
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ ${template.title} │
|
||||||
|
│ ${template.subtitle} │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 👤 GASTINFORMATIONEN: │
|
||||||
|
│ Name: ${template.guest_info.name} │
|
||||||
|
│ Anfrage-ID: ${template.guest_info.request_id} │
|
||||||
|
│ E-Mail: ${template.guest_info.email} │
|
||||||
|
│ Genehmigt: ${template.guest_info.approved_at || 'N/A'} │
|
||||||
|
│ │
|
||||||
|
│ 🔑 ZUGANGSDATEN: │
|
||||||
|
│ OTP-Code: ${template.access_data.otp_code} │
|
||||||
|
│ Gültig bis: ${template.access_data.valid_until} │
|
||||||
|
│ │
|
||||||
|
│ 🌐 SYSTEMZUGANG: │
|
||||||
|
│ Terminal vor Ort oder │
|
||||||
|
│ ${template.access_data.login_url} │
|
||||||
|
│ │
|
||||||
|
│ 📋 NUTZUNGSREGELN: │
|
||||||
|
${template.usage_rules.map(rule => `│ • ${rule}`).join('\n')} │
|
||||||
|
│ │
|
||||||
|
│ 📍 ABHOLUNG SPÄTER: │
|
||||||
|
│ Ort: ${template.pickup_info.location} │
|
||||||
|
│ Zeit: ${template.pickup_info.hours} │
|
||||||
|
│ Lagerung: ${template.pickup_info.storage_days} │
|
||||||
|
│ │
|
||||||
|
│ [QR-Code für System-Login] │
|
||||||
|
│ │
|
||||||
|
│ 📞 Bei Fragen: Mercedes-Benz Ansprechpartner │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
👨💼 ${template.admin_note}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePrintModal() {
|
||||||
|
document.getElementById('print-modal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function printTemplate() {
|
||||||
|
window.print();
|
||||||
|
closePrintModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification functions
|
||||||
|
function showSuccess(message) {
|
||||||
|
// Simple success notification
|
||||||
|
alert('✅ ' + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
// Simple error notification
|
||||||
|
alert('❌ ' + message);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
Reference in New Issue
Block a user