🔧 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:
2025-06-16 01:27:41 +02:00
parent b1ae9523a9
commit b5c758c971
4 changed files with 842 additions and 25 deletions

View File

@ -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_login import login_required, current_user
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
# ===== BLUEPRINT-KONFIGURATION =====
@ -1283,6 +1283,214 @@ def export_logs_api():
admin_logger.error(f"Fehler beim Exportieren der Logs: {str(e)}")
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 =====
@admin_api_blueprint.route("/system/status", methods=["GET"])

View File

@ -380,24 +380,29 @@ def api_create_guest_request():
@guest_blueprint.route('/api/guest/start-job', methods=['POST'])
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
def api_start_job_with_code():
"""Job mit 6-stelligem OTP-Code starten."""
"""Job mit Name + 6-stelligem OTP-Code starten (Offline-System)."""
try:
data = request.get_json()
if not data or 'code' not in data:
return jsonify({"error": "Code ist erforderlich"}), 400
if not data or 'code' not in data or 'name' not in data:
return jsonify({"error": "Name und Code sind erforderlich"}), 400
code = data['code'].strip().upper()
name = data['name'].strip()
if len(code) != 6:
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:
# Gastanfrage anhand des OTP-Codes finden
matching_request = GuestRequest.find_by_otp(code)
# Gastanfrage anhand des OTP-Codes UND Names finden
matching_request = GuestRequest.find_by_otp_and_name(code, name)
if not matching_request:
return jsonify({
"success": False,
"error": "Ungültiger oder bereits verwendeter Code"
"error": "Ungültiger Code oder Name stimmt nicht überein"
}), 400
# 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
def api_guest_status_by_otp():
"""
Öffentliche Route für Gäste um ihren Auftragsstatus mit OTP-Code zu prüfen.
Keine Authentifizierung erforderlich.
Öffentliche Route für Gäste um ihren Auftragsstatus mit Name + OTP-Code zu prüfen.
Keine Authentifizierung erforderlich (Offline-System).
"""
try:
data = request.get_json()
@ -1009,7 +1014,7 @@ def api_guest_status_by_otp():
}), 400
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:
return jsonify({
@ -1017,26 +1022,21 @@ def api_guest_status_by_otp():
'message': 'OTP-Code ist erforderlich'
}), 400
if not name:
return jsonify({
'success': False,
'message': 'Name ist erforderlich'
}), 400
with get_cached_session() as db_session:
# 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
# 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 für Gast-Status-Abfrage: {otp_code[:4]}****")
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 E-Mail-Adresse'
'message': 'Ungültiger Code oder Name stimmt nicht überein'
}), 404
# Status-Informationen für den Gast zusammenstellen

View File

@ -1228,6 +1228,41 @@ class GuestRequest(Base):
except Exception as e:
logger.error(f"Fehler beim Suchen der Gastanfrage per OTP: {str(e)}")
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):

View 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 %}