1473 lines
54 KiB
HTML
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">×</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">×</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">×</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 = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
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 %} |