/** * Mercedes-Benz MYP Admin Guest Requests Management * Moderne Verwaltung von Gastaufträgen mit Live-Updates */ // Globale Variablen let currentRequests = []; let filteredRequests = []; let currentPage = 0; let totalPages = 0; let totalRequests = 0; let refreshInterval = null; let csrfToken = ''; // API Base URL Detection - Korrigierte Version für CSP-Kompatibilität function detectApiBaseUrl() { // Für lokale Entwicklung und CSP-Kompatibilität immer relative URLs verwenden // Das verhindert CSP-Probleme mit connect-src return ''; // Leerer String für relative URLs } const API_BASE_URL = detectApiBaseUrl(); // Initialisierung beim Laden der Seite document.addEventListener('DOMContentLoaded', function() { // CSRF Token abrufen csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; // Event Listeners initialisieren initEventListeners(); // Daten initial laden loadGuestRequests(); // Auto-Refresh starten startAutoRefresh(); console.log('🎯 Admin Guest Requests Management geladen'); }); /** * Event Listeners initialisieren */ function initEventListeners() { // Search Input const searchInput = document.getElementById('search-requests'); if (searchInput) { searchInput.addEventListener('input', debounce(handleSearch, 300)); } // Status Filter const statusFilter = document.getElementById('status-filter'); if (statusFilter) { statusFilter.addEventListener('change', handleFilterChange); } // Sort Order const sortOrder = document.getElementById('sort-order'); if (sortOrder) { sortOrder.addEventListener('change', handleSortChange); } // Action Buttons const refreshBtn = document.getElementById('refresh-btn'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { loadGuestRequests(); showNotification('🔄 Gastaufträge aktualisiert', 'info'); }); } const exportBtn = document.getElementById('export-btn'); if (exportBtn) { exportBtn.addEventListener('click', handleExport); } const bulkActionsBtn = document.getElementById('bulk-actions-btn'); if (bulkActionsBtn) { bulkActionsBtn.addEventListener('click', showBulkActionsModal); } // Select All Checkbox const selectAllCheckbox = document.getElementById('select-all'); if (selectAllCheckbox) { selectAllCheckbox.addEventListener('change', handleSelectAll); } } /** * Gastaufträge von der API laden */ async function loadGuestRequests() { try { showLoading(true); const url = `${API_BASE_URL}/api/admin/requests`; const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.success) { currentRequests = data.requests || []; totalRequests = data.total || 0; // Statistiken aktualisieren updateStats(data.stats || {}); // Tabelle aktualisieren applyFiltersAndSort(); console.log(`✅ ${currentRequests.length} Gastaufträge geladen`); } else { throw new Error(data.message || 'Fehler beim Laden der Gastaufträge'); } } catch (error) { console.error('Fehler beim Laden der Gastaufträge:', error); showNotification('❌ Fehler beim Laden der Gastaufträge: ' + error.message, 'error'); showEmptyState(); } finally { showLoading(false); } } /** * Statistiken aktualisieren */ function updateStats(stats) { const elements = { 'pending-count': stats.pending || 0, 'approved-count': stats.approved || 0, 'rejected-count': stats.rejected || 0, 'total-count': stats.total || 0 }; Object.entries(elements).forEach(([id, value]) => { const element = document.getElementById(id); if (element) { animateCounter(element, value); } }); } /** * Counter mit Animation */ function animateCounter(element, targetValue) { const currentValue = parseInt(element.textContent) || 0; const difference = targetValue - currentValue; const steps = 20; const stepValue = difference / steps; let step = 0; const interval = setInterval(() => { step++; const value = Math.round(currentValue + (stepValue * step)); element.textContent = value; if (step >= steps) { clearInterval(interval); element.textContent = targetValue; } }, 50); } /** * Filter und Sortierung anwenden */ function applyFiltersAndSort() { let requests = [...currentRequests]; // Status Filter const statusFilter = document.getElementById('status-filter')?.value; if (statusFilter && statusFilter !== 'all') { requests = requests.filter(req => req.status === statusFilter); } // Such-Filter const searchTerm = document.getElementById('search-requests')?.value.toLowerCase(); if (searchTerm) { requests = requests.filter(req => req.name?.toLowerCase().includes(searchTerm) || req.email?.toLowerCase().includes(searchTerm) || req.file_name?.toLowerCase().includes(searchTerm) || req.reason?.toLowerCase().includes(searchTerm) ); } // Sortierung const sortOrder = document.getElementById('sort-order')?.value; requests.sort((a, b) => { switch (sortOrder) { case 'oldest': return new Date(a.created_at) - new Date(b.created_at); case 'priority': return getPriorityValue(b) - getPriorityValue(a); case 'newest': default: return new Date(b.created_at) - new Date(a.created_at); } }); filteredRequests = requests; renderRequestsTable(); } /** * Prioritätswert für Sortierung berechnen */ function getPriorityValue(request) { const now = new Date(); const created = new Date(request.created_at); const hoursOld = (now - created) / (1000 * 60 * 60); let priority = 0; // Status-basierte Priorität if (request.status === 'pending') priority += 10; else if (request.status === 'approved') priority += 5; // Alter-basierte Priorität if (hoursOld > 24) priority += 5; else if (hoursOld > 8) priority += 3; else if (hoursOld > 2) priority += 1; return priority; } /** * Requests-Tabelle rendern */ function renderRequestsTable() { const tableBody = document.getElementById('requests-table-body'); const emptyState = document.getElementById('empty-state'); if (!tableBody) return; if (filteredRequests.length === 0) { tableBody.innerHTML = ''; showEmptyState(); return; } hideEmptyState(); const requestsHtml = filteredRequests.map(request => createRequestRow(request)).join(''); tableBody.innerHTML = requestsHtml; // Event Listeners für neue Rows hinzufügen addRowEventListeners(); } /** * Request Row HTML erstellen */ function createRequestRow(request) { const statusColor = getStatusColor(request.status); const priorityLevel = getPriorityLevel(request); const timeAgo = getTimeAgo(request.created_at); return `
${request.name ? request.name[0].toUpperCase() : 'G'}
${escapeHtml(request.name || 'Unbekannt')}
${escapeHtml(request.email || 'Keine E-Mail')}
${escapeHtml(request.file_name || 'Keine Datei')}
${request.duration_minutes ? `${request.duration_minutes} Min.` : 'Unbekannte Dauer'} ${request.copies ? ` • ${request.copies} Kopien` : ''}
${request.reason ? `
${escapeHtml(request.reason)}
` : ''} ${getStatusText(request.status)}
${timeAgo}
${formatDateTime(request.created_at)}
${getPriorityBadge(priorityLevel)} ${request.is_urgent ? '🔥 Dringend' : ''}
${request.status === 'pending' ? ` ` : ''} ${request.status === 'approved' ? ` ` : ''}
`; } /** * Status-Helper-Funktionen */ function getStatusColor(status) { const colors = { 'pending': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300', 'approved': 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', 'rejected': 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', 'expired': 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300' }; return colors[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300'; } function getStatusDot(status) { const dots = { 'pending': 'bg-yellow-400 dark:bg-yellow-300', 'approved': 'bg-green-400 dark:bg-green-300', 'rejected': 'bg-red-400 dark:bg-red-300', 'expired': 'bg-gray-400 dark:bg-gray-300' }; return dots[status] || 'bg-gray-400 dark:bg-gray-300'; } function getStatusText(status) { const texts = { 'pending': 'Wartend', 'approved': 'Genehmigt', 'rejected': 'Abgelehnt', 'expired': 'Abgelaufen' }; return texts[status] || status; } function getPriorityLevel(request) { const priority = getPriorityValue(request); if (priority >= 15) return 'high'; if (priority >= 8) return 'medium'; return 'low'; } function getPriorityBadge(level) { const badges = { 'high': '🔴 Hoch', 'medium': '🟡 Mittel', 'low': '🟢 Niedrig' }; return badges[level] || badges['low']; } /** * CRUD-Operationen */ async function approveRequest(requestId) { const notes = prompt('Genehmigungsnotizen (optional):'); if (notes === null) return; // User clicked cancel try { showLoading(true); const url = `${API_BASE_URL}/api/requests/${requestId}/approve`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ notes: notes || '' }) }); const data = await response.json(); if (data.success) { showNotification('✅ Gastauftrag erfolgreich genehmigt', 'success'); if (data.otp) { showNotification(`🔑 OTP-Code für Gast: ${data.otp}`, 'info'); } loadGuestRequests(); } else { throw new Error(data.message || 'Fehler beim Genehmigen'); } } catch (error) { console.error('Fehler beim Genehmigen:', error); showNotification('❌ Fehler beim Genehmigen: ' + error.message, 'error'); } finally { showLoading(false); } } async function rejectRequest(requestId) { const reason = prompt('Grund für die Ablehnung (erforderlich):'); if (!reason || reason.trim() === '') { showNotification('⚠️ Ablehnungsgrund ist erforderlich', 'warning'); return; } try { showLoading(true); const url = `${API_BASE_URL}/api/requests/${requestId}/deny`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ reason: reason.trim() }) }); const data = await response.json(); if (data.success) { showNotification('✅ Gastauftrag erfolgreich abgelehnt', 'success'); loadGuestRequests(); } else { throw new Error(data.message || 'Fehler beim Ablehnen'); } } catch (error) { console.error('Fehler beim Ablehnen:', error); showNotification('❌ Fehler beim Ablehnen: ' + error.message, 'error'); } finally { showLoading(false); } } async function deleteRequest(requestId) { if (!confirm('Möchten Sie diesen Gastauftrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) return; try { showLoading(true); const url = `${API_BASE_URL}/api/admin/requests/${requestId}`; const response = await fetch(url, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { showNotification('✅ Gastauftrag erfolgreich gelöscht', 'success'); loadGuestRequests(); } else { throw new Error(data.message || 'Fehler beim Löschen'); } } catch (error) { console.error('Fehler beim Löschen:', error); showNotification('❌ Fehler beim Löschen: ' + error.message, 'error'); } finally { showLoading(false); } } /** * Detail-Modal Funktionen */ function showRequestDetail(requestId) { const request = currentRequests.find(req => req.id === requestId); if (!request) return; const modal = document.getElementById('detail-modal'); const content = document.getElementById('modal-content'); content.innerHTML = `

Gastauftrag Details

Antragsteller

Name: ${escapeHtml(request.name || 'Unbekannt')}

E-Mail: ${escapeHtml(request.email || 'Keine E-Mail')}

Erstellt am: ${formatDateTime(request.created_at)}

Auftrag Details

Datei: ${escapeHtml(request.file_name || 'Keine Datei')}

Dauer: ${request.duration_minutes || 'Unbekannt'} Minuten

Kopien: ${request.copies || 1}

Status: ${getStatusText(request.status)}

${request.reason ? `

Begründung

${escapeHtml(request.reason)}

` : ''}
${request.status === 'pending' ? ` ` : ''}
`; modal.classList.remove('hidden'); } function closeDetailModal() { const modal = document.getElementById('detail-modal'); modal.classList.add('hidden'); } /** * Bulk Actions */ function showBulkActionsModal() { const selectedIds = getSelectedRequestIds(); if (selectedIds.length === 0) { showNotification('⚠️ Bitte wählen Sie mindestens einen Gastauftrag aus', 'warning'); return; } const modal = document.getElementById('bulk-modal'); modal.classList.remove('hidden'); } function closeBulkModal() { const modal = document.getElementById('bulk-modal'); modal.classList.add('hidden'); } async function performBulkAction(action) { const selectedIds = getSelectedRequestIds(); if (selectedIds.length === 0) return; const confirmMessages = { 'approve': `Möchten Sie ${selectedIds.length} Gastaufträge genehmigen?`, 'reject': `Möchten Sie ${selectedIds.length} Gastaufträge ablehnen?`, 'delete': `Möchten Sie ${selectedIds.length} Gastaufträge löschen?` }; if (!confirm(confirmMessages[action])) return; try { showLoading(true); closeBulkModal(); const promises = selectedIds.map(async (id) => { let url, method, body = null; if (action === 'approve') { url = `${API_BASE_URL}/api/requests/${id}/approve`; method = 'POST'; body = JSON.stringify({ notes: '' }); } else if (action === 'reject') { url = `${API_BASE_URL}/api/requests/${id}/deny`; method = 'POST'; body = JSON.stringify({ reason: 'Bulk-Ablehnung durch Administrator' }); } else if (action === 'delete') { url = `${API_BASE_URL}/api/admin/requests/${id}`; method = 'DELETE'; } return fetch(url, { method, headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: body }); }); const results = await Promise.allSettled(promises); const successCount = results.filter(r => r.status === 'fulfilled' && r.value.ok).length; showNotification(`✅ ${successCount} von ${selectedIds.length} Aktionen erfolgreich`, 'success'); loadGuestRequests(); // Alle Checkboxen zurücksetzen document.getElementById('select-all').checked = false; document.querySelectorAll('.request-checkbox').forEach(cb => cb.checked = false); } catch (error) { console.error('Fehler bei Bulk-Aktion:', error); showNotification('❌ Fehler bei der Bulk-Aktion: ' + error.message, 'error'); } finally { showLoading(false); } } function getSelectedRequestIds() { const checkboxes = document.querySelectorAll('.request-checkbox:checked'); return Array.from(checkboxes).map(cb => parseInt(cb.value)); } /** * Event Handlers */ function handleSearch() { applyFiltersAndSort(); } function handleFilterChange() { applyFiltersAndSort(); } function handleSortChange() { applyFiltersAndSort(); } function handleSelectAll(event) { const checkboxes = document.querySelectorAll('.request-checkbox'); checkboxes.forEach(checkbox => { checkbox.checked = event.target.checked; }); } function handleExport() { const selectedIds = getSelectedRequestIds(); const exportData = selectedIds.length > 0 ? filteredRequests.filter(req => selectedIds.includes(req.id)) : filteredRequests; if (exportData.length === 0) { showNotification('⚠️ Keine Daten zum Exportieren verfügbar', 'warning'); return; } exportToCSV(exportData); } /** * Export-Funktionen */ function exportToCSV(data) { const headers = ['ID', 'Name', 'E-Mail', 'Datei', 'Status', 'Erstellt', 'Dauer (Min)', 'Kopien', 'Begründung']; const rows = data.map(req => [ req.id, req.name || '', req.email || '', req.file_name || '', getStatusText(req.status), formatDateTime(req.created_at), req.duration_minutes || '', req.copies || '', req.reason || '' ]); const csvContent = [headers, ...rows] .map(row => row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `gastauftraege_${new Date().toISOString().split('T')[0]}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } showNotification('📄 CSV-Export erfolgreich erstellt', 'success'); } /** * Auto-Refresh */ function startAutoRefresh() { // Refresh alle 30 Sekunden refreshInterval = setInterval(() => { loadGuestRequests(); }, 30000); } function stopAutoRefresh() { if (refreshInterval) { clearInterval(refreshInterval); refreshInterval = null; } } /** * Utility-Funktionen */ function addRowEventListeners() { // Falls notwendig, können hier zusätzliche Event Listener hinzugefügt werden } function showLoading(show) { const loadingElement = document.getElementById('table-loading'); const tableBody = document.getElementById('requests-table-body'); if (loadingElement) { loadingElement.classList.toggle('hidden', !show); } if (show && tableBody) { tableBody.innerHTML = ''; } } function showEmptyState() { const emptyState = document.getElementById('empty-state'); if (emptyState) { emptyState.classList.remove('hidden'); } } function hideEmptyState() { const emptyState = document.getElementById('empty-state'); if (emptyState) { emptyState.classList.add('hidden'); } } function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `fixed top-4 right-4 px-6 py-4 rounded-xl shadow-2xl z-50 transform transition-all duration-500 translate-x-full ${ type === 'success' ? 'bg-green-500 text-white' : type === 'error' ? 'bg-red-500 text-white' : type === 'warning' ? 'bg-yellow-500 text-black' : 'bg-blue-500 text-white' }`; notification.innerHTML = `
${type === 'success' ? '✅' : type === 'error' ? '❌' : type === 'warning' ? '⚠️' : 'ℹ️'} ${message}
`; document.body.appendChild(notification); // Animation einblenden setTimeout(() => { notification.classList.remove('translate-x-full'); }, 100); // Nach 5 Sekunden entfernen setTimeout(() => { notification.classList.add('translate-x-full'); setTimeout(() => notification.remove(), 5000); }, 5000); } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text ? String(text).replace(/[&<>"']/g, m => map[m]) : ''; } function formatDateTime(dateString) { if (!dateString) return 'Unbekannt'; const date = new Date(dateString); return date.toLocaleString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } function getTimeAgo(dateString) { if (!dateString) return 'Unbekannt'; const now = new Date(); const date = new Date(dateString); const diffInSeconds = Math.floor((now - date) / 1000); if (diffInSeconds < 60) return 'Gerade eben'; if (diffInSeconds < 3600) return `vor ${Math.floor(diffInSeconds / 60)} Min.`; if (diffInSeconds < 86400) return `vor ${Math.floor(diffInSeconds / 3600)} Std.`; if (diffInSeconds < 2592000) return `vor ${Math.floor(diffInSeconds / 86400)} Tagen`; return formatDateTime(dateString); } /** * OTP-Code-Verwaltung */ // OTP-Code für genehmigte Anfrage anzeigen async function showOtpCode(requestId) { try { showLoading(true); const url = `${API_BASE_URL}/api/admin/requests/${requestId}/otp`; const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.success && data.otp_code) { showOtpModal(data.otp_code, data.request_id, data.expires_at, data.status); } else { showNotification('❌ ' + (data.error || 'OTP-Code konnte nicht abgerufen werden'), 'error'); } } catch (error) { console.error('Fehler beim Abrufen des OTP-Codes:', error); showNotification('❌ Fehler beim Abrufen des OTP-Codes: ' + error.message, 'error'); } finally { showLoading(false); } } // OTP-Modal anzeigen function showOtpModal(otpCode, requestId, expiresAt, status) { // Bestehende Modals schließen closeOtpModal(); const modal = document.createElement('div'); modal.className = 'fixed inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4'; modal.id = 'otp-modal'; const expiryDate = expiresAt ? new Date(expiresAt).toLocaleString('de-DE') : 'Unbekannt'; const statusText = status === 'used' ? 'Bereits verwendet' : status === 'expired' ? 'Abgelaufen' : 'Gültig'; const statusColor = status === 'used' ? 'text-red-600 dark:text-red-400' : status === 'expired' ? 'text-orange-600 dark:text-orange-400' : 'text-green-600 dark:text-green-400'; modal.innerHTML = `

OTP-Code

${otpCode}

6-stelliger Zugangscode für den Gast

Status: ${statusText}
Gültig bis: ${expiryDate}
Anfrage-ID: #${requestId}

Der Gast kann diesen Code auf der Startseite eingeben, um seinen genehmigten Druckauftrag zu starten.

`; document.body.appendChild(modal); // Animation einblenden setTimeout(() => { const content = document.getElementById('otp-modal-content'); if (content) { content.classList.remove('scale-95', 'opacity-0'); content.classList.add('scale-100', 'opacity-100'); } }, 10); // Modal schließen bei Klick außerhalb modal.addEventListener('click', function(e) { if (e.target === modal) { closeOtpModal(); } }); // ESC-Taste zum Schließen const escapeHandler = function(e) { if (e.key === 'Escape') { closeOtpModal(); document.removeEventListener('keydown', escapeHandler); } }; document.addEventListener('keydown', escapeHandler); } // OTP-Code in Zwischenablage kopieren function copyOtpCode(code) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(code).then(() => { showNotification('📋 OTP-Code in Zwischenablage kopiert', 'success'); }).catch(() => { fallbackCopyTextToClipboard(code); }); } else { fallbackCopyTextToClipboard(code); } } // Fallback für ältere Browser 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) { showNotification('📋 OTP-Code in Zwischenablage kopiert', 'success'); } else { showNotification('❌ Fehler beim Kopieren des Codes', 'error'); } } catch (err) { showNotification('❌ Fehler beim Kopieren des Codes', 'error'); } document.body.removeChild(textArea); } // OTP-Modal schließen function closeOtpModal() { const modal = document.getElementById('otp-modal'); if (modal) { const content = document.getElementById('otp-modal-content'); if (content) { content.classList.add('scale-95', 'opacity-0'); content.classList.remove('scale-100', 'opacity-100'); } setTimeout(() => { modal.remove(); }, 300); } } // Genehmigung widerrufen async function revokeRequest(requestId) { if (!confirm('Möchten Sie die Genehmigung dieser Anfrage wirklich widerrufen? Der OTP-Code wird ungültig.')) { return; } try { showLoading(true); const url = `${API_BASE_URL}/api/requests/${requestId}/deny`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ reason: 'Genehmigung durch Administrator widerrufen' }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.success) { showNotification('✅ Genehmigung erfolgreich widerrufen', 'success'); loadGuestRequests(); // Tabelle aktualisieren } else { showNotification('❌ ' + (data.error || 'Fehler beim Widerrufen der Genehmigung'), 'error'); } } catch (error) { console.error('Fehler beim Widerrufen der Genehmigung:', error); showNotification('❌ Fehler beim Widerrufen der Genehmigung: ' + error.message, 'error'); } finally { showLoading(false); } } // Globale Funktionen für onclick-Handler window.showRequestDetail = showRequestDetail; window.approveRequest = approveRequest; window.rejectRequest = rejectRequest; window.deleteRequest = deleteRequest; window.closeDetailModal = closeDetailModal; window.closeBulkModal = closeBulkModal; window.performBulkAction = performBulkAction; console.log('📋 Admin Guest Requests JavaScript vollständig geladen');