manage-your-printer/templates/admin_guest_requests_overview.html
2025-06-04 10:03:22 +02:00

1473 lines
54 KiB
HTML

{% extends "base.html" %}
{% block title %}Gastanträge Verwaltung{% endblock %}
{% block extra_css %}
<style>
.stats-card {
transition: transform 0.2s ease-in-out;
}
.stats-card:hover {
transform: translateY(-2px);
}
.urgent-request {
background-color: #fef2f2;
border-left: 4px solid #ef4444;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.status-pending {
background-color: #fef3c7;
color: #d97706;
}
.status-approved {
background-color: #d1fae5;
color: #059669;
}
.status-rejected {
background-color: #fee2e2;
color: #dc2626;
}
.action-button {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border-radius: 0.375rem;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.action-button:hover {
transform: scale(1.05);
}
.btn-approve {
background-color: #10b981;
color: white;
}
.btn-approve:hover {
background-color: #059669;
}
.btn-reject {
background-color: #ef4444;
color: white;
}
.btn-reject:hover {
background-color: #dc2626;
}
.btn-view {
background-color: #3b82f6;
color: white;
}
.btn-view:hover {
background-color: #2563eb;
}
/* Enhanced Inline Form Styles */
.inline-form {
animation: slideDown 0.3s ease-out;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
max-height: 0;
}
to {
opacity: 1;
transform: translateY(0);
max-height: 200px;
}
}
.inline-approve-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
transition: all 0.2s ease;
}
.inline-approve-btn:hover {
background: linear-gradient(135deg, #059669 0%, #047857 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.inline-reject-btn {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
transition: all 0.2s ease;
}
.inline-reject-btn:hover {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.inline-form textarea:focus,
.inline-form select:focus {
outline: none;
ring: 2px;
ring-opacity: 50%;
transform: scale(1.02);
transition: all 0.2s ease;
}
.inline-approval-form {
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
border: 2px solid #a7f3d0;
}
.inline-rejection-form {
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
border: 2px solid #fca5a5;
}
/* Enhanced table styles */
.admin-table {
border-collapse: separate;
border-spacing: 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.admin-table th {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
font-weight: 600;
color: #374151;
padding: 1rem 1.5rem;
}
.admin-table td {
padding: 1rem 1.5rem;
vertical-align: top;
border-bottom: 1px solid #e5e7eb;
}
.admin-table tr:hover {
background-color: #f9fafb;
transition: background-color 0.2s ease;
}
/* Status column enhancements */
.status-column {
min-width: 200px;
}
.inline-actions {
margin-top: 0.5rem;
}
.inline-action-btn {
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
border-radius: 6px;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.form-section {
background: white;
border-radius: 8px;
padding: 1rem;
margin-top: 0.5rem;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.form-section h5 {
margin: 0 0 0.5rem 0;
font-weight: 600;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 0.75rem;
}
.btn-cancel {
background: #f3f4f6;
color: #6b7280;
border: 1px solid #d1d5db;
}
.btn-cancel:hover {
background: #e5e7eb;
color: #374151;
}
.btn-submit {
font-weight: 500;
}
/* Loading states */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-spinner {
border: 3px solid #f3f4f6;
border-top: 3px solid #3b82f6;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.admin-table th {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
color: #f9fafb;
}
.admin-table tr:hover {
background-color: #1f2937;
}
.inline-approval-form {
background: linear-gradient(135deg, #064e3b 0%, #065f46 100%);
border-color: #047857;
}
.inline-rejection-form {
background: linear-gradient(135deg, #7f1d1d 0%, #991b1b 100%);
border-color: #dc2626;
}
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(0, 115, 206, 0.2) transparent;
}
/* Modal Scrollbar Styling */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-track {
background: transparent;
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb {
background: rgba(0, 115, 206, 0.2);
border-radius: 4px;
transition: all 0.3s ease;
}
.modal-content::-webkit-scrollbar-thumb:hover {
background: rgba(0, 115, 206, 0.4);
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.spinner {
border: 4px solid #f3f4f6;
border-top: 4px solid #3b82f6;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-3xl font-bold text-gray-900">Gastanträge Verwaltung</h1>
<p class="text-gray-600 mt-1">Verwalten Sie alle Gastanfragen für 3D-Druckaufträge mit direkter Genehmigung/Ablehnung</p>
<div class="flex items-center mt-2 text-sm text-blue-600">
<i class="fas fa-bolt mr-1"></i>
<span>Neue Funktionen: Direkte Aktionen in der Tabelle • Verbesserte Fehlerbehandlung</span>
</div>
</div>
<div class="flex space-x-2">
<button id="refreshBtn" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors">
<i class="fas fa-sync-alt mr-2"></i>Aktualisieren
</button>
<button id="exportBtn" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition-colors">
<i class="fas fa-download mr-2"></i>Exportieren
</button>
</div>
</div>
<!-- Statistiken -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="stats-card bg-white p-6 rounded-lg shadow-lg">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
<i class="fas fa-list-ul text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-sm font-medium text-gray-500">Gesamt</h3>
<p class="text-2xl font-bold text-gray-900" id="total-count">-</p>
</div>
</div>
</div>
<div class="stats-card bg-white p-6 rounded-lg shadow-lg">
<div class="flex items-center">
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600">
<i class="fas fa-clock text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-sm font-medium text-gray-500">Offen</h3>
<p class="text-2xl font-bold text-yellow-600" id="pending-count">-</p>
</div>
</div>
</div>
<div class="stats-card bg-white p-6 rounded-lg shadow-lg">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600">
<i class="fas fa-check text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-sm font-medium text-gray-500">Genehmigt</h3>
<p class="text-2xl font-bold text-green-600" id="approved-count">-</p>
</div>
</div>
</div>
<div class="stats-card bg-white p-6 rounded-lg shadow-lg">
<div class="flex items-center">
<div class="p-3 rounded-full bg-red-100 text-red-600">
<i class="fas fa-times text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-sm font-medium text-gray-500">Abgelehnt</h3>
<p class="text-2xl font-bold text-red-600" id="rejected-count">-</p>
</div>
</div>
</div>
</div>
<!-- Filter und Suche -->
<div class="bg-white p-4 rounded-lg shadow mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label for="statusFilter" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select id="statusFilter" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="all">Alle Status</option>
<option value="pending">Offen</option>
<option value="approved">Genehmigt</option>
<option value="rejected">Abgelehnt</option>
</select>
</div>
<div>
<label for="urgentFilter" class="block text-sm font-medium text-gray-700 mb-1">Priorität</label>
<select id="urgentFilter" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="all">Alle</option>
<option value="urgent">Nur dringende</option>
<option value="normal">Normal</option>
</select>
</div>
<div>
<label for="sortOrder" class="block text-sm font-medium text-gray-700 mb-1">Sortierung</label>
<select id="sortOrder" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="newest">Neueste zuerst</option>
<option value="oldest">Älteste zuerst</option>
<option value="urgent">Dringende zuerst</option>
</select>
</div>
<div>
<label for="searchInput" class="block text-sm font-medium text-gray-700 mb-1">Suchen</label>
<input type="text" id="searchInput" placeholder="Name, E-Mail oder Datei..."
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="loading">
<div class="spinner"></div>
<p class="mt-2 text-gray-600">Lade Gastanträge...</p>
</div>
<!-- Anträge Liste -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Gastanträge</h3>
<p class="text-sm text-gray-600 mt-1">Alle eingegangenen Anfragen für Gastzugang</p>
</div>
<div class="overflow-x-auto">
<table class="min-w-full admin-table">
<thead>
<tr>
<th class="text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Antragsteller
</th>
<th class="text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Datei & Details
</th>
<th class="status-column text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status & Aktionen
</th>
<th class="text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Erstellt
</th>
<th class="text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Verwaltung
</th>
</tr>
</thead>
<tbody id="requestsTable" class="divide-y divide-gray-200">
<!-- Wird dynamisch gefüllt -->
</tbody>
</table>
</div>
<!-- Pagination -->
<div id="paginationContainer" class="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<div class="text-sm text-gray-700">
Zeige <span id="resultsInfo">0-0 von 0</span> Einträgen
</div>
<div class="flex space-x-2">
<button id="prevPageBtn" class="px-3 py-1 border border-gray-300 rounded text-sm hover:bg-gray-50 disabled:opacity-50" disabled>
Zurück
</button>
<button id="nextPageBtn" class="px-3 py-1 border border-gray-300 rounded text-sm hover:bg-gray-50 disabled:opacity-50" disabled>
Weiter
</button>
</div>
</div>
</div>
</div>
<!-- Genehmigungsmodal -->
<div id="approveModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2 class="text-xl font-bold mb-4">Antrag genehmigen</h2>
<form id="approveForm">
<div class="mb-4">
<label for="approvalNotes" class="block text-sm font-medium text-gray-700 mb-2">
Genehmigungsnotizen (optional)
</label>
<textarea id="approvalNotes" rows="3"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Zusätzliche Notizen oder Anweisungen..."></textarea>
</div>
<div class="mb-4">
<label for="assignedPrinter" class="block text-sm font-medium text-gray-700 mb-2">
Drucker zuweisen (optional)
</label>
<select id="assignedPrinter" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Kein Drucker zugewiesen</option>
<!-- Wird dynamisch gefüllt -->
</select>
</div>
<div class="flex justify-end space-x-2">
<button type="button" id="cancelApproval" class="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-50">
Abbrechen
</button>
<button type="submit" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">
Genehmigen
</button>
</div>
</form>
</div>
</div>
<!-- Ablehnungsmodal -->
<div id="rejectModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2 class="text-xl font-bold mb-4">Antrag ablehnen</h2>
<form id="rejectForm">
<div class="mb-4">
<label for="rejectionReason" class="block text-sm font-medium text-gray-700 mb-2">
Ablehnungsgrund <span class="text-red-500">*</span>
</label>
<textarea id="rejectionReason" rows="3" required
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Bitte geben Sie einen Grund für die Ablehnung an..."></textarea>
</div>
<div class="flex justify-end space-x-2">
<button type="button" id="cancelRejection" class="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-50">
Abbrechen
</button>
<button type="submit" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">
Ablehnen
</button>
</div>
</form>
</div>
</div>
<!-- Detailmodal -->
<div id="detailModal" class="modal">
<div class="modal-content" style="max-width: 700px;">
<span class="close">&times;</span>
<h2 class="text-xl font-bold mb-4">Antrag Details</h2>
<div id="detailContent">
<!-- Wird dynamisch gefüllt -->
</div>
</div>
</div>
<!-- Notification Toast -->
<div id="notification" class="fixed top-4 right-4 p-4 rounded-lg shadow-lg hidden transition-all duration-300 z-50">
<div class="flex items-center">
<span id="notificationIcon" class="mr-2"></span>
<span id="notificationMessage"></span>
<button id="notificationClose" class="ml-4 text-white hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Admin-Berechtigung als Data-Attribute setzen -->
<div id="adminConfig"
data-is-admin="{% if current_user.is_authenticated and current_user.is_admin %}true{% else %}false{% endif %}"
data-can-approve="{% if current_user.is_authenticated and current_user.permissions and current_user.permissions.can_approve_jobs %}true{% else %}false{% endif %}"
style="display: none;"></div>
<script>
// Globale Variablen
let currentRequests = [];
let currentPage = 0;
let pageSize = 20;
let totalRequests = 0;
let currentRequestId = null;
let allPrinters = [];
// Admin-Berechtigung aus Data-Attributen lesen
const adminConfig = document.getElementById('adminConfig');
const userIsAdmin = adminConfig.dataset.isAdmin === 'true';
const userCanApprove = adminConfig.dataset.canApprove === 'true';
const showInlineActions = userIsAdmin || userCanApprove;
console.log('🔍 DEBUG: Admin-Berechtigungen:', {
userIsAdmin,
userCanApprove,
showInlineActions,
adminConfigElement: adminConfig,
dataIsAdmin: adminConfig.dataset.isAdmin,
dataCanApprove: adminConfig.dataset.canApprove
});
// Debug: Zeige alle Data-Attribute
console.log('🔍 DEBUG: Alle Data-Attribute:', adminConfig.dataset);
// Debug: Prüfe HTML-Inhalt
console.log('🔍 DEBUG: AdminConfig HTML:', adminConfig.outerHTML);
// Initialisierung
document.addEventListener('DOMContentLoaded', function() {
console.log('🚀 DEBUG: DOMContentLoaded - Initialisierung startet');
console.log('🔍 DEBUG: showInlineActions beim Laden:', showInlineActions);
initializeEventListeners();
loadAvailablePrinters();
loadGuestRequests();
});
function initializeEventListeners() {
// Filter und Suche
document.getElementById('refreshBtn').addEventListener('click', () => loadGuestRequests());
document.getElementById('exportBtn').addEventListener('click', exportRequests);
document.getElementById('statusFilter').addEventListener('change', () => {
currentPage = 0;
loadGuestRequests();
});
document.getElementById('urgentFilter').addEventListener('change', () => {
currentPage = 0;
loadGuestRequests();
});
document.getElementById('sortOrder').addEventListener('change', () => {
currentPage = 0;
loadGuestRequests();
});
document.getElementById('searchInput').addEventListener('input', debounce(() => {
currentPage = 0;
loadGuestRequests();
}, 500));
// Pagination
document.getElementById('prevPageBtn').addEventListener('click', () => {
if (currentPage > 0) {
currentPage--;
loadGuestRequests();
}
});
document.getElementById('nextPageBtn').addEventListener('click', () => {
if ((currentPage + 1) * pageSize < totalRequests) {
currentPage++;
loadGuestRequests();
}
});
// Modal Event Listeners
setupModalEventListeners();
}
function setupModalEventListeners() {
// Modals schließen
document.querySelectorAll('.close').forEach(closeBtn => {
closeBtn.addEventListener('click', closeAllModals);
});
// Cancel buttons
document.getElementById('cancelApproval').addEventListener('click', closeAllModals);
document.getElementById('cancelRejection').addEventListener('click', closeAllModals);
// Form submissions
document.getElementById('approveForm').addEventListener('submit', handleApproval);
document.getElementById('rejectForm').addEventListener('submit', handleRejection);
// Notification close
document.getElementById('notificationClose').addEventListener('click', hideNotification);
// Close modals when clicking outside
window.addEventListener('click', function(event) {
if (event.target.classList.contains('modal')) {
closeAllModals();
}
});
}
async function loadGuestRequests() {
try {
showLoading(true);
const status = document.getElementById('statusFilter').value;
const urgent = document.getElementById('urgentFilter').value;
const sort = document.getElementById('sortOrder').value;
const search = document.getElementById('searchInput').value.trim();
const params = new URLSearchParams({
page: currentPage,
page_size: pageSize,
status: status !== 'all' ? status : '',
sort: sort,
search: search
});
if (urgent !== 'all') {
params.append('urgent', urgent === 'urgent');
}
const response = await fetch(`/api/admin/guest-requests?${params}`);
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;
updateStatistics(data.stats);
renderRequestsTable(data.requests);
updatePagination();
} else {
throw new Error(data.message || 'Unbekannter Fehler beim Laden der Daten');
}
} catch (error) {
console.error('Fehler beim Laden der Gastanträge:', error);
showNotification(`Fehler beim Laden der Gastanträge: ${error.message}`, 'error');
// Zeige leere Tabelle bei Fehler
renderRequestsTable([]);
updateStatistics({});
} finally {
showLoading(false);
}
}
async function loadAvailablePrinters() {
try {
const response = await fetch('/api/printers');
const data = await response.json();
if (data.success) {
allPrinters = data.printers;
const select = document.getElementById('assignedPrinter');
select.innerHTML = '<option value="">Kein Drucker zugewiesen</option>';
data.printers.forEach(printer => {
const option = document.createElement('option');
option.value = printer.id;
option.textContent = `${printer.name} (${printer.location || 'Unbekannt'})`;
select.appendChild(option);
});
}
} catch (error) {
console.error('Fehler beim Laden der Drucker:', error);
}
}
function updateStatistics(stats) {
document.getElementById('total-count').textContent = stats.total || 0;
document.getElementById('pending-count').textContent = stats.pending || 0;
document.getElementById('approved-count').textContent = stats.approved || 0;
document.getElementById('rejected-count').textContent = stats.rejected || 0;
}
function renderRequestsTable(requests) {
const tbody = document.getElementById('requestsTable');
tbody.innerHTML = '';
if (requests.length === 0) {
const row = document.createElement('tr');
row.innerHTML = `
<td colspan="5" class="text-center py-12">
<div class="flex flex-col items-center">
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<i class="fas fa-inbox text-2xl text-gray-400"></i>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Keine Gastanträge gefunden</h3>
<p class="text-gray-500 max-w-sm">
Es gibt derzeit keine Anträge mit den ausgewählten Filterkriterien.
Versuchen Sie andere Filter oder warten Sie auf neue Anfragen.
</p>
</div>
</td>
`;
tbody.appendChild(row);
return;
}
requests.forEach(request => {
const row = createRequestRow(request);
tbody.appendChild(row);
});
}
function createRequestRow(request) {
const row = document.createElement('tr');
// Berechne ob dringend
const now = new Date();
const createdAt = new Date(request.created_at);
const hoursOld = (now - createdAt) / (1000 * 60 * 60);
const isUrgent = hoursOld > 24 && request.status === 'pending';
// DEBUG: Zeige Request-Details
console.log('🔍 DEBUG: createRequestRow für Request:', {
id: request.id,
name: request.name,
status: request.status,
isPending: request.status === 'pending',
showInlineActions: showInlineActions,
shouldShowButtons: request.status === 'pending' && showInlineActions
});
if (isUrgent) {
row.classList.add('urgent-request');
}
// DEBUG: Button-HTML generieren
const buttonHtml = request.status === 'pending' && showInlineActions ? `
<div id="inline-actions-${request.id}" class="mt-2 space-y-2">
<div class="flex space-x-2">
<button onclick="showInlineApproval(${request.id})"
class="inline-flex items-center px-2 py-1 border border-green-300 rounded text-xs text-green-700 bg-green-50 hover:bg-green-100 transition-colors">
<i class="fas fa-check mr-1"></i>Genehmigen
</button>
<button onclick="showInlineRejection(${request.id})"
class="inline-flex items-center px-2 py-1 border border-red-300 rounded text-xs text-red-700 bg-red-50 hover:bg-red-100 transition-colors">
<i class="fas fa-times mr-1"></i>Ablehnen
</button>
</div>
<!-- Inline Approval Form -->
<div id="approval-form-${request.id}" class="hidden inline-approval-form form-section">
<h5 class="text-sm font-medium text-green-800 mb-2">
<i class="fas fa-check-circle mr-1"></i>Antrag genehmigen
</h5>
<textarea id="approval-notes-${request.id}" rows="2"
class="w-full text-xs border border-green-300 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-green-400"
placeholder="Optionale Genehmigungsnotizen..."></textarea>
<select id="approval-printer-${request.id}"
class="w-full mt-1 text-xs border border-green-300 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-green-400">
<option value="">Drucker auswählen (optional)</option>
</select>
<div class="form-actions">
<button onclick="hideInlineApproval(${request.id})"
class="btn-cancel inline-action-btn">
<i class="fas fa-times mr-1"></i>Abbrechen
</button>
<button onclick="submitInlineApproval(${request.id})"
class="btn-submit inline-action-btn inline-approve-btn">
<i class="fas fa-check mr-1"></i>Genehmigen
</button>
</div>
</div>
<!-- Inline Rejection Form -->
<div id="rejection-form-${request.id}" class="hidden inline-rejection-form form-section">
<h5 class="text-sm font-medium text-red-800 mb-2">
<i class="fas fa-times-circle mr-1"></i>Antrag ablehnen
</h5>
<textarea id="rejection-reason-${request.id}" rows="2" required
class="w-full text-xs border border-red-300 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-red-400"
placeholder="Ablehnungsgrund (erforderlich)..."></textarea>
<div class="form-actions">
<button onclick="hideInlineRejection(${request.id})"
class="btn-cancel inline-action-btn">
<i class="fas fa-times mr-1"></i>Abbrechen
</button>
<button onclick="submitInlineRejection(${request.id})"
class="btn-submit inline-action-btn inline-reject-btn">
<i class="fas fa-times mr-1"></i>Ablehnen
</button>
</div>
</div>
</div>
` : '';
console.log('🔍 DEBUG: Button-HTML für Request', request.id, ':', buttonHtml ? 'GENERIERT' : 'LEER');
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div>
<div class="text-sm font-medium text-gray-900">
${escapeHtml(request.name)}
${isUrgent ? '<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800"><i class="fas fa-exclamation-triangle mr-1"></i>Dringend</span>' : ''}
</div>
<div class="text-sm text-gray-500">${escapeHtml(request.email || 'Keine E-Mail')}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">
<i class="fas fa-file mr-1"></i>${escapeHtml(request.file_name || 'Keine Datei')}
</div>
<div class="text-sm text-gray-500">
<i class="fas fa-clock mr-1"></i>${request.duration_minutes || 0} Min,
<i class="fas fa-copy ml-2 mr-1"></i>${request.copies || 1} Kopien
</div>
${request.reason ? `<div class="text-xs text-gray-400 mt-1">${escapeHtml(request.reason.substring(0, 100))}${request.reason.length > 100 ? '...' : ''}</div>` : ''}
</td>
<td class="px-6 py-4">
<span class="status-badge status-${request.status}">
${getStatusIcon(request.status)} ${getStatusText(request.status)}
</span>
${buttonHtml}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div>${formatDate(request.created_at)}</div>
<div class="text-xs text-gray-400">${formatTimeAgo(request.created_at)}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2">
<button onclick="showRequestDetails(${request.id})"
class="action-button btn-view" title="Details anzeigen">
<i class="fas fa-eye"></i> Details
</button>
${request.status !== 'pending' && showInlineActions ? `
<button onclick="deleteRequest(${request.id})"
class="action-button bg-gray-500 text-white hover:bg-gray-600" title="Löschen">
<i class="fas fa-trash"></i>
</button>
` : ''}
</div>
</td>
`;
return row;
}
function getStatusIcon(status) {
switch (status) {
case 'pending': return '<i class="fas fa-clock"></i>';
case 'approved': return '<i class="fas fa-check"></i>';
case 'rejected': return '<i class="fas fa-times"></i>';
default: return '<i class="fas fa-question"></i>';
}
}
function getStatusText(status) {
switch (status) {
case 'pending': return 'Offen';
case 'approved': return 'Genehmigt';
case 'rejected': return 'Abgelehnt';
default: return 'Unbekannt';
}
}
function showApproveModal(requestId) {
currentRequestId = requestId;
document.getElementById('approveModal').style.display = 'block';
document.getElementById('approvalNotes').value = '';
document.getElementById('assignedPrinter').value = '';
}
function showRejectModal(requestId) {
currentRequestId = requestId;
document.getElementById('rejectModal').style.display = 'block';
document.getElementById('rejectionReason').value = '';
}
async function showRequestDetails(requestId) {
try {
const response = await fetch(`/api/guest-requests/${requestId}`);
const data = await response.json();
if (data.success) {
const request = data.request;
const detailContent = document.getElementById('detailContent');
detailContent.innerHTML = `
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 class="font-semibold text-gray-900 mb-2">Antragsteller</h3>
<p><strong>Name:</strong> ${escapeHtml(request.name)}</p>
<p><strong>E-Mail:</strong> ${escapeHtml(request.email || 'Nicht angegeben')}</p>
<p><strong>IP-Adresse:</strong> ${escapeHtml(request.author_ip || 'Unbekannt')}</p>
</div>
<div>
<h3 class="font-semibold text-gray-900 mb-2">Druckdetails</h3>
<p><strong>Dateiname:</strong> ${escapeHtml(request.file_name || 'Keine Datei')}</p>
<p><strong>Dauer:</strong> ${request.duration_minutes || 0} Minuten</p>
<p><strong>Kopien:</strong> ${request.copies || 1}</p>
${request.file_size_mb ? `<p><strong>Dateigröße:</strong> ${request.file_size_mb} MB</p>` : ''}
</div>
<div class="md:col-span-2">
<h3 class="font-semibold text-gray-900 mb-2">Grund</h3>
<p class="bg-gray-50 p-3 rounded">${escapeHtml(request.reason || 'Kein Grund angegeben')}</p>
</div>
<div>
<h3 class="font-semibold text-gray-900 mb-2">Status</h3>
<p><strong>Aktueller Status:</strong> <span class="status-badge status-${request.status}">${getStatusText(request.status)}</span></p>
<p><strong>Erstellt am:</strong> ${formatDate(request.created_at)}</p>
${request.updated_at ? `<p><strong>Aktualisiert am:</strong> ${formatDate(request.updated_at)}</p>` : ''}
</div>
<div>
<h3 class="font-semibold text-gray-900 mb-2">Bearbeitung</h3>
${request.approved_at ? `
<p><strong>Genehmigt am:</strong> ${formatDate(request.approved_at)}</p>
${request.approved_by_name ? `<p><strong>Genehmigt von:</strong> ${escapeHtml(request.approved_by_name)}</p>` : ''}
${request.approval_notes ? `<p><strong>Notizen:</strong> ${escapeHtml(request.approval_notes)}</p>` : ''}
${request.otp_code ? `<p><strong>OTP-Code:</strong> <code class="bg-gray-100 px-2 py-1 rounded">${request.otp_code}</code></p>` : ''}
` : ''}
${request.rejected_at ? `
<p><strong>Abgelehnt am:</strong> ${formatDate(request.rejected_at)}</p>
${request.rejected_by_name ? `<p><strong>Abgelehnt von:</strong> ${escapeHtml(request.rejected_by_name)}</p>` : ''}
${request.rejection_reason ? `<p><strong>Grund:</strong> ${escapeHtml(request.rejection_reason)}</p>` : ''}
` : ''}
</div>
</div>
`;
document.getElementById('detailModal').style.display = 'block';
} else {
showNotification('Fehler beim Laden der Details: ' + (data.message || 'Unbekannter Fehler'), 'error');
}
} catch (error) {
console.error('Fehler beim Laden der Details:', error);
showNotification('Fehler beim Laden der Details', 'error');
}
}
async function handleApproval(event) {
event.preventDefault();
if (!currentRequestId) return;
const notes = document.getElementById('approvalNotes').value.trim();
const printerId = document.getElementById('assignedPrinter').value;
const requestBody = { notes };
if (printerId) {
requestBody.printer_id = parseInt(printerId);
}
try {
const response = await fetch(`/api/guest-requests/${currentRequestId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify(requestBody)
});
// Enhanced error handling for JSON parsing
let data;
try {
const text = await response.text();
if (!text.trim()) {
throw new Error('Leere Antwort vom Server');
}
data = JSON.parse(text);
} catch (parseError) {
console.error('JSON Parse Error:', parseError);
throw new Error(`JSON-Parsing-Fehler: ${parseError.message}`);
}
if (data.success) {
showNotification(`Antrag erfolgreich genehmigt. OTP-Code: ${data.otp_code}`, 'success');
closeAllModals();
loadGuestRequests();
} else {
showNotification('Fehler beim Genehmigen: ' + (data.message || 'Unbekannter Fehler'), 'error');
}
} catch (error) {
console.error('Fehler beim Genehmigen:', error);
showNotification('Fehler beim Genehmigen: ' + error.message, 'error');
}
}
async function handleRejection(event) {
event.preventDefault();
if (!currentRequestId) return;
const reason = document.getElementById('rejectionReason').value.trim();
if (!reason) {
showNotification('Bitte geben Sie einen Ablehnungsgrund an', 'error');
return;
}
try {
const response = await fetch(`/api/guest-requests/${currentRequestId}/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({ reason })
});
// Enhanced error handling for JSON parsing
let data;
try {
const text = await response.text();
if (!text.trim()) {
throw new Error('Leere Antwort vom Server');
}
data = JSON.parse(text);
} catch (parseError) {
console.error('JSON Parse Error:', parseError);
throw new Error(`JSON-Parsing-Fehler: ${parseError.message}`);
}
if (data.success) {
showNotification('Antrag erfolgreich abgelehnt', 'success');
closeAllModals();
loadGuestRequests();
} else {
showNotification('Fehler beim Ablehnen: ' + (data.message || 'Unbekannter Fehler'), 'error');
}
} catch (error) {
console.error('Fehler beim Ablehnen:', error);
showNotification('Fehler beim Ablehnen: ' + error.message, 'error');
}
}
async function exportRequests() {
try {
const status = document.getElementById('statusFilter').value;
const params = new URLSearchParams();
if (status !== 'all') {
params.append('status', status);
}
const response = await fetch(`/api/admin/guest-requests/export?${params}`);
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `gastantraege_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showNotification('Export erfolgreich heruntergeladen', 'success');
} else {
showNotification('Fehler beim Export', 'error');
}
} catch (error) {
console.error('Fehler beim Export:', error);
showNotification('Fehler beim Export: ' + error.message, 'error');
}
}
function updatePagination() {
const start = currentPage * pageSize + 1;
const end = Math.min((currentPage + 1) * pageSize, totalRequests);
document.getElementById('resultsInfo').textContent = `${start}-${end} von ${totalRequests}`;
document.getElementById('prevPageBtn').disabled = currentPage === 0;
document.getElementById('nextPageBtn').disabled = (currentPage + 1) * pageSize >= totalRequests;
}
function closeAllModals() {
document.querySelectorAll('.modal').forEach(modal => {
modal.style.display = 'none';
});
currentRequestId = null;
}
function showLoading(show) {
document.getElementById('loadingIndicator').style.display = show ? 'block' : 'none';
}
function showNotification(message, type = 'info') {
const notification = document.getElementById('notification');
const icon = document.getElementById('notificationIcon');
const messageEl = document.getElementById('notificationMessage');
// Icon und Farbe basierend auf Type
let iconClass, bgClass;
switch (type) {
case 'success':
iconClass = 'fas fa-check-circle';
bgClass = 'bg-green-500';
break;
case 'error':
iconClass = 'fas fa-exclamation-circle';
bgClass = 'bg-red-500';
break;
case 'warning':
iconClass = 'fas fa-exclamation-triangle';
bgClass = 'bg-yellow-500';
break;
default:
iconClass = 'fas fa-info-circle';
bgClass = 'bg-blue-500';
}
icon.className = iconClass;
notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg text-white transition-all duration-300 z-50 ${bgClass}`;
messageEl.textContent = message;
notification.classList.remove('hidden');
// Auto-hide nach 5 Sekunden
setTimeout(() => {
hideNotification();
}, 5000);
}
function hideNotification() {
document.getElementById('notification').classList.add('hidden');
}
function getCsrfToken() {
return document.querySelector('meta[name=csrf-token]')?.getAttribute('content') || '';
}
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) {
if (!text) return '';
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
function formatDate(dateString) {
if (!dateString) return 'Unbekannt';
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function formatTimeAgo(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diffInMinutes = Math.floor((now - date) / (1000 * 60));
if (diffInMinutes < 60) {
return `vor ${diffInMinutes} Min`;
} else if (diffInMinutes < 1440) {
return `vor ${Math.floor(diffInMinutes / 60)} Std`;
} else {
return `vor ${Math.floor(diffInMinutes / 1440)} Tagen`;
}
}
// Inline approval/rejection functions
function showInlineApproval(requestId) {
hideInlineRejection(requestId); // Hide rejection form if open
document.getElementById(`approval-form-${requestId}`).classList.remove('hidden');
// Populate printer dropdown
populatePrinterDropdown(`approval-printer-${requestId}`);
}
function hideInlineApproval(requestId) {
document.getElementById(`approval-form-${requestId}`).classList.add('hidden');
document.getElementById(`approval-notes-${requestId}`).value = '';
document.getElementById(`approval-printer-${requestId}`).value = '';
}
function showInlineRejection(requestId) {
hideInlineApproval(requestId); // Hide approval form if open
document.getElementById(`rejection-form-${requestId}`).classList.remove('hidden');
}
function hideInlineRejection(requestId) {
document.getElementById(`rejection-form-${requestId}`).classList.add('hidden');
document.getElementById(`rejection-reason-${requestId}`).value = '';
}
async function populatePrinterDropdown(selectId) {
try {
const response = await fetch('/api/printers');
// Enhanced error handling for JSON parsing
let data;
try {
const text = await response.text();
if (!text.trim()) {
throw new Error('Leere Antwort vom Server');
}
data = JSON.parse(text);
} catch (parseError) {
console.error('JSON Parse Error:', parseError);
throw new Error('Ungültige JSON-Antwort vom Server');
}
if (data.success) {
const select = document.getElementById(selectId);
select.innerHTML = '<option value="">Drucker auswählen (optional)</option>';
data.printers.forEach(printer => {
const option = document.createElement('option');
option.value = printer.id;
option.textContent = `${printer.name} (${printer.location || 'Unbekannt'})`;
select.appendChild(option);
});
}
} catch (error) {
console.error('Fehler beim Laden der Drucker:', error);
showNotification(`Fehler beim Laden der Drucker: ${error.message}`, 'error');
}
}
async function submitInlineApproval(requestId) {
const notes = document.getElementById(`approval-notes-${requestId}`).value.trim();
const printerId = document.getElementById(`approval-printer-${requestId}`).value;
const requestBody = { notes };
if (printerId) {
requestBody.printer_id = parseInt(printerId);
}
try {
// Loading-Zustand anzeigen
const submitBtn = document.querySelector(`#approval-form-${requestId} .inline-approve-btn`);
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Wird bearbeitet...';
submitBtn.disabled = true;
// KORRIGIERTE API-URL
const response = await fetch(`/api/guest-requests/${requestId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
showNotification(`Antrag erfolgreich genehmigt! OTP-Code: ${data.otp_code}`, 'success');
hideInlineApproval(requestId);
loadGuestRequests(); // Reload table
} else {
throw new Error(data.message || 'Unbekannter Fehler beim Genehmigen');
}
} catch (error) {
console.error('Fehler beim Genehmigen:', error);
showNotification(`Fehler beim Genehmigen: ${error.message}`, 'error');
// Button-Zustand zurücksetzen
const submitBtn = document.querySelector(`#approval-form-${requestId} .inline-approve-btn`);
if (submitBtn) {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
}
async function submitInlineRejection(requestId) {
const reason = document.getElementById(`rejection-reason-${requestId}`).value.trim();
if (!reason) {
showNotification('Bitte geben Sie einen Ablehnungsgrund an', 'error');
return;
}
try {
// Loading-Zustand anzeigen
const submitBtn = document.querySelector(`#rejection-form-${requestId} .inline-reject-btn`);
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Wird bearbeitet...';
submitBtn.disabled = true;
// KORRIGIERTE API-URL
const response = await fetch(`/api/guest-requests/${requestId}/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({ reason })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
showNotification('Antrag erfolgreich abgelehnt', 'success');
hideInlineRejection(requestId);
loadGuestRequests(); // Reload table
} else {
throw new Error(data.message || 'Unbekannter Fehler beim Ablehnen');
}
} catch (error) {
console.error('Fehler beim Ablehnen:', error);
showNotification(`Fehler beim Ablehnen: ${error.message}`, 'error');
// Button-Zustand zurücksetzen
const submitBtn = document.querySelector(`#rejection-form-${requestId} .inline-reject-btn`);
if (submitBtn) {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
}
async function deleteRequest(requestId) {
if (!confirm('Sind Sie sicher, dass Sie diesen Antrag permanent löschen möchten?')) {
return;
}
try {
const response = await fetch(`/api/guest-requests/${requestId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': getCsrfToken()
}
});
// Enhanced error handling for JSON parsing
let data;
try {
const text = await response.text();
if (!text.trim()) {
throw new Error('Leere Antwort vom Server');
}
data = JSON.parse(text);
} catch (parseError) {
console.error('JSON Parse Error:', parseError);
throw new Error(`JSON-Parsing-Fehler: ${parseError.message}`);
}
if (data.success) {
showNotification('Antrag erfolgreich gelöscht', 'success');
loadGuestRequests(); // Reload table
} else {
showNotification(`Fehler beim Löschen: ${data.message || 'Unbekannter Fehler'}`, 'error');
}
} catch (error) {
console.error('Fehler beim Löschen:', error);
showNotification(`Fehler beim Löschen: ${error.message}`, 'error');
}
}
</script>
{% endblock %}