Files
Projektarbeit-MYP/backend/templates/admin_guest_otps.html
Till Tomczak 472060ab1f 🔧 Update: Enhance Guest Request Model with OTP Code Management
**Änderungen:**
-  Hinzugefügt: `otp_code_plain` zur `GuestRequest`-Klasse für die Speicherung des OTP-Codes im Klartext zur Anzeige für Administratoren.
-  Anpassung der API-Endpunkte in `admin_unified.py`, um den Klartext-OTP-Code anzuzeigen, wenn die Anfrage genehmigt ist und der OTP-Code aktiv ist.

**Ergebnis:**
- Verbesserte Verwaltung und Sichtbarkeit von OTP-Codes für Administratoren, was die Benutzerfreundlichkeit und Sicherheit bei der Verwaltung von Gastanfragen erhöht.

🤖 Generated with [Claude Code](https://claude.ai/code)
2025-06-16 01:39:37 +02:00

723 lines
28 KiB
HTML

{% 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;
backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.8);
}
.dark .otp-card {
background: rgba(15, 23, 42, 0.8);
}
.otp-card.critical {
border-left-color: #ef4444;
animation: pulseBorder 2s infinite;
background: rgba(254, 242, 242, 0.8);
}
.dark .otp-card.critical {
background: rgba(127, 29, 29, 0.2);
}
.otp-card.warning {
border-left-color: #f59e0b;
background: rgba(255, 251, 235, 0.8);
}
.dark .otp-card.warning {
background: rgba(120, 53, 15, 0.2);
}
.print-template {
font-family: 'Courier New', monospace;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
white-space: pre-line;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.dark .print-template {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
border-color: #475569;
color: #e2e8f0;
}
@keyframes pulseBorder {
0%, 100% { border-left-width: 4px; }
50% { border-left-width: 6px; }
}
.otp-code-display {
font-family: 'Courier New', monospace;
font-size: 1.5rem;
font-weight: bold;
letter-spacing: 0.1em;
padding: 0.75rem 1rem;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
border-radius: 8px;
text-align: center;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.glass-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark .glass-card {
background: rgba(15, 23, 42, 0.2);
border-color: rgba(148, 163, 184, 0.1);
}
</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 glass-card rounded-xl">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center space-x-3 mb-3">
<span class="text-2xl">🔑</span>
<p class="text-sm font-medium text-blue-900 dark:text-blue-300">OTP-Code</p>
</div>
<div class="otp-code-display mb-3">
${request.otp_code}
</div>
<div class="flex items-center space-x-4 text-sm">
<div class="flex items-center space-x-1">
<span class="text-green-500">⏰</span>
<span class="text-blue-700 dark:text-blue-400">
Gültig bis: ${request.otp_expires_at ? new Date(request.otp_expires_at).toLocaleString('de-DE') : '-'}
</span>
</div>
<div class="flex items-center space-x-1">
<span class="text-yellow-500">📋</span>
<span class="text-blue-700 dark:text-blue-400">
Status: ${request.otp_status === 'active' ? 'Aktiv' : 'Inaktiv'}
</span>
</div>
</div>
</div>
<div class="ml-4">
<button onclick="copyOTPToClipboard('${request.otp_code}')"
class="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm transition-colors">
📋 Kopieren
</button>
</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 glass-card rounded-xl mb-3 ${otp.urgency === 'critical' ? 'border-l-4 border-red-500' : otp.urgency === 'warning' ? 'border-l-4 border-yellow-500' : 'border-l-4 border-green-500'}">
<div class="flex items-center space-x-4">
<div class="p-3 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full">
<span class="text-xl text-white">🔑</span>
</div>
<div>
<p class="font-semibold text-gray-900 dark:text-white text-lg">${otp.guest_name}</p>
<div class="flex items-center space-x-3 text-sm">
<div class="flex items-center space-x-1">
<span class="text-orange-500">⏰</span>
<span class="text-gray-600 dark:text-gray-400">
Läuft ab in ${otp.hours_remaining}h
</span>
</div>
<span class="px-2 py-1 rounded-full text-xs font-medium ${getUrgencyBadgeClass(otp.urgency)}">
${getUrgencyText(otp.urgency)}
</span>
</div>
</div>
</div>
<div class="text-right">
<div class="otp-code-display text-base mb-2">
${otp.otp_code}
</div>
<button onclick="copyOTPToClipboard('${otp.otp_code}')"
class="px-3 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-xs transition-colors">
📋 Kopieren
</button>
</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();
}
// Utility functions
function copyOTPToClipboard(otpCode) {
if (navigator.clipboard && window.isSecureContext) {
// Use modern clipboard API
navigator.clipboard.writeText(otpCode).then(() => {
showSuccess(`OTP-Code "${otpCode}" in Zwischenablage kopiert`);
}).catch(err => {
console.error('Fehler beim Kopieren:', err);
fallbackCopyTextToClipboard(otpCode);
});
} else {
// Fallback für ältere Browser
fallbackCopyTextToClipboard(otpCode);
}
}
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
showSuccess(`OTP-Code "${text}" in Zwischenablage kopiert`);
} else {
showError('Kopieren fehlgeschlagen');
}
} catch (err) {
console.error('Fallback-Kopieren fehlgeschlagen:', err);
showError('Kopieren nicht unterstützt');
}
document.body.removeChild(textArea);
}
// Notification functions
function showSuccess(message) {
// Enhanced notification with auto-hide
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transition-all duration-300';
notification.innerHTML = `
<div class="flex items-center space-x-2">
<span class="text-lg">✅</span>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// Auto-hide after 3 seconds
setTimeout(() => {
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
function showError(message) {
// Enhanced notification with auto-hide
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transition-all duration-300';
notification.innerHTML = `
<div class="flex items-center space-x-2">
<span class="text-lg">❌</span>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// Auto-hide after 5 seconds
setTimeout(() => {
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 5000);
}
</script>
{% endblock %}