🔧 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)
This commit is contained in:
@ -279,6 +279,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
TBA-Anträge
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('admin.guest_otps_management') }}"
|
||||
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'guest_otps' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
|
||||
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'guest_otps' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" 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 2m0 0a2 2 0 012 2m-4-4v8a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h2m0 0V6a2 2 0 012-2h2a2 2 0 012 2v1m-4 0h4"/>
|
||||
</svg>
|
||||
🔑 OTP-Codes
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('admin.tapo_monitoring') }}"
|
||||
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'tapo_monitoring' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
|
||||
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'tapo_monitoring' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
@ -9,25 +9,69 @@
|
||||
.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: pulse 2s infinite;
|
||||
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: monospace;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
padding: 1rem;
|
||||
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);
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); }
|
||||
.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 %}
|
||||
@ -326,16 +370,36 @@ function renderGuestRequests() {
|
||||
` : ''}
|
||||
|
||||
${request.otp_code && request.status === 'approved' ? `
|
||||
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div class="mt-4 p-4 glass-card rounded-xl">
|
||||
<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">
|
||||
<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}
|
||||
</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 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>
|
||||
@ -373,25 +437,34 @@ function renderActiveOTPs() {
|
||||
}
|
||||
|
||||
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 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-2 bg-blue-100 dark:bg-blue-900 rounded-full">
|
||||
<span class="text-lg">🔑</span>
|
||||
<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">${otp.guest_name}</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Läuft ab in ${otp.hours_remaining}h
|
||||
</p>
|
||||
<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">
|
||||
<p class="text-xl font-mono font-bold text-blue-600 dark:text-blue-400">
|
||||
<div class="otp-code-display text-base mb-2">
|
||||
${otp.otp_code}
|
||||
</p>
|
||||
<span class="px-2 py-1 rounded text-xs font-medium ${getUrgencyBadgeClass(otp.urgency)}">
|
||||
${getUrgencyText(otp.urgency)}
|
||||
</span>
|
||||
</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('');
|
||||
@ -560,15 +633,91 @@ function printTemplate() {
|
||||
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) {
|
||||
// Simple success notification
|
||||
alert('✅ ' + 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) {
|
||||
// Simple error notification
|
||||
alert('❌ ' + 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 %}
|
Reference in New Issue
Block a user