609 lines
24 KiB
HTML
609 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Gastanfragen verwalten{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container mx-auto px-4 py-8">
|
|
<div class="bg-white rounded-lg shadow-lg p-6">
|
|
<!-- Header -->
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-3xl font-bold text-gray-800">
|
|
<i class="fas fa-user-friends mr-3 text-blue-600"></i>
|
|
Gastanfragen verwalten
|
|
</h1>
|
|
<div class="flex items-center space-x-4">
|
|
<div class="stats-summary flex space-x-4" id="statsContainer">
|
|
<!-- Statistiken werden hier geladen -->
|
|
</div>
|
|
<button onclick="refreshRequests()" class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
|
|
<i class="fas fa-sync-alt mr-2"></i>Aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter und Such-Bar -->
|
|
<div class="mb-6 flex flex-wrap gap-4 items-center">
|
|
<div class="flex items-center space-x-2">
|
|
<label for="statusFilter" class="text-sm font-medium text-gray-700">Status:</label>
|
|
<select id="statusFilter" class="form-select rounded-md border-gray-300" onchange="filterRequests()">
|
|
<option value="all">Alle</option>
|
|
<option value="pending" selected>Wartend</option>
|
|
<option value="approved">Genehmigt</option>
|
|
<option value="denied">Abgelehnt</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<label for="searchInput" class="text-sm font-medium text-gray-700">Suchen:</label>
|
|
<input type="text" id="searchInput" placeholder="Name, E-Mail..."
|
|
class="form-input rounded-md border-gray-300 w-64"
|
|
oninput="debounceSearch()">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Anfragen-Liste -->
|
|
<div id="requestsContainer" class="space-y-4">
|
|
<!-- Wird dynamisch geladen -->
|
|
</div>
|
|
|
|
<!-- Loading Spinner -->
|
|
<div id="loadingSpinner" class="text-center py-8 hidden">
|
|
<i class="fas fa-spinner fa-spin text-3xl text-blue-600"></i>
|
|
<p class="text-gray-600 mt-2">Lade Anfragen...</p>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div id="paginationContainer" class="mt-6 flex justify-center">
|
|
<!-- Wird dynamisch geladen -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail-Modal -->
|
|
<div id="detailModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden">
|
|
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
|
|
<div class="mt-3">
|
|
<!-- Modal Header -->
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-medium text-gray-900" id="modalTitle">Anfrage Details</h3>
|
|
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times text-xl"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Modal Content -->
|
|
<div id="modalContent">
|
|
<!-- Wird dynamisch geladen -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Aktions-Modal (Genehmigen/Ablehnen) -->
|
|
<div id="actionModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden">
|
|
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-lg shadow-lg rounded-md bg-white">
|
|
<div class="mt-3">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-medium text-gray-900" id="actionModalTitle">Anfrage bearbeiten</h3>
|
|
<button onclick="closeActionModal()" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times text-xl"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="actionForm">
|
|
<input type="hidden" id="actionRequestId">
|
|
<input type="hidden" id="actionType">
|
|
|
|
<!-- Genehmigen-Sektion -->
|
|
<div id="approveSection" class="hidden">
|
|
<div class="mb-4">
|
|
<label for="printerSelect" class="block text-sm font-medium text-gray-700 mb-2">
|
|
Drucker zuweisen:
|
|
</label>
|
|
<select id="printerSelect" class="form-select w-full rounded-md border-gray-300">
|
|
<option value="">Drucker auswählen...</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="approvalNotes" class="block text-sm font-medium text-gray-700 mb-2">
|
|
Notizen (optional):
|
|
</label>
|
|
<textarea id="approvalNotes" rows="3"
|
|
class="form-textarea w-full rounded-md border-gray-300"
|
|
placeholder="Zusätzliche Anweisungen oder Hinweise..."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ablehnen-Sektion -->
|
|
<div id="denySection" class="hidden">
|
|
<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="4" required
|
|
class="form-textarea w-full rounded-md border-gray-300"
|
|
placeholder="Bitte geben Sie einen detaillierten Grund für die Ablehnung an..."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3 mt-6">
|
|
<button type="button" onclick="closeActionModal()"
|
|
class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400">
|
|
Abbrechen
|
|
</button>
|
|
<button type="submit" id="actionSubmitBtn"
|
|
class="px-4 py-2 rounded-md text-white">
|
|
<!-- Text wird dynamisch gesetzt -->
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.badge {
|
|
@apply px-2 py-1 rounded-full text-xs font-medium;
|
|
}
|
|
.badge-pending {
|
|
@apply bg-yellow-100 text-yellow-800;
|
|
}
|
|
.badge-approved {
|
|
@apply bg-green-100 text-green-800;
|
|
}
|
|
.badge-denied {
|
|
@apply bg-red-100 text-red-800;
|
|
}
|
|
.request-card {
|
|
@apply border rounded-lg p-4 hover:shadow-md transition-shadow;
|
|
}
|
|
.request-card.urgent {
|
|
@apply border-orange-300 bg-orange-50;
|
|
}
|
|
.stats-badge {
|
|
@apply px-3 py-1 rounded-full text-sm font-medium;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let currentRequests = [];
|
|
let currentPage = 0;
|
|
let totalPages = 0;
|
|
let searchTimeout;
|
|
|
|
// Beim Laden der Seite
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadAvailablePrinters();
|
|
refreshRequests();
|
|
|
|
// Form-Handler
|
|
document.getElementById('actionForm').addEventListener('submit', handleActionSubmit);
|
|
});
|
|
|
|
// Anfragen laden
|
|
async function loadRequests(status = 'pending', offset = 0, search = '') {
|
|
try {
|
|
showLoading(true);
|
|
|
|
const params = new URLSearchParams({
|
|
status: status,
|
|
limit: 20,
|
|
offset: offset
|
|
});
|
|
|
|
const response = await fetch(`/api/admin/requests?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
currentRequests = data.requests;
|
|
displayRequests(data.requests, search);
|
|
displayStats(data.stats);
|
|
displayPagination(data.pagination);
|
|
} else {
|
|
showError('Fehler beim Laden der Anfragen: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
showError('Fehler beim Laden der Anfragen: ' + error.message);
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
// Anfragen anzeigen
|
|
function displayRequests(requests, search = '') {
|
|
const container = document.getElementById('requestsContainer');
|
|
|
|
// Filtern nach Suchbegriff
|
|
let filteredRequests = requests;
|
|
if (search) {
|
|
const searchLower = search.toLowerCase();
|
|
filteredRequests = requests.filter(req =>
|
|
req.name.toLowerCase().includes(searchLower) ||
|
|
(req.email && req.email.toLowerCase().includes(searchLower)) ||
|
|
(req.reason && req.reason.toLowerCase().includes(searchLower))
|
|
);
|
|
}
|
|
|
|
if (filteredRequests.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="text-center py-8 text-gray-500">
|
|
<i class="fas fa-inbox text-4xl mb-4"></i>
|
|
<p class="text-lg">Keine Anfragen gefunden</p>
|
|
<p class="text-sm">Versuchen Sie einen anderen Filter oder Suchbegriff</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = filteredRequests.map(req => createRequestCard(req)).join('');
|
|
}
|
|
|
|
// Anfrage-Karte erstellen
|
|
function createRequestCard(request) {
|
|
const urgentClass = request.time_since_creation > 24 ? 'urgent' : '';
|
|
const statusBadge = getStatusBadge(request.status);
|
|
|
|
return `
|
|
<div class="request-card ${urgentClass}" data-request-id="${request.id}">
|
|
<div class="flex justify-between items-start">
|
|
<div class="flex-1">
|
|
<div class="flex items-center space-x-3 mb-2">
|
|
<h3 class="text-lg font-semibold text-gray-800">${escapeHtml(request.name)}</h3>
|
|
${statusBadge}
|
|
${request.time_since_creation > 24 ? '<span class="badge bg-orange-100 text-orange-800">Dringend</span>' : ''}
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600">
|
|
<div>
|
|
<p><i class="fas fa-envelope mr-2"></i>${escapeHtml(request.email || 'Keine E-Mail')}</p>
|
|
<p><i class="fas fa-clock mr-2"></i>${request.duration_min} Minuten</p>
|
|
<p><i class="fas fa-calendar mr-2"></i>${formatDateTime(request.created_at)}</p>
|
|
</div>
|
|
<div>
|
|
<p><i class="fas fa-print mr-2"></i>${request.printer ? escapeHtml(request.printer.name) : 'Kein Drucker'}</p>
|
|
${request.processed_by_user ? `<p><i class="fas fa-user mr-2"></i>Bearbeitet von: ${escapeHtml(request.processed_by_user.name)}</p>` : ''}
|
|
${request.processed_at ? `<p><i class="fas fa-check mr-2"></i>${formatDateTime(request.processed_at)}</p>` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
${request.reason ? `<p class="mt-2 text-sm text-gray-700 bg-gray-50 p-2 rounded">${escapeHtml(request.reason)}</p>` : ''}
|
|
${request.approval_notes ? `<p class="mt-2 text-sm text-green-700 bg-green-50 p-2 rounded"><strong>Genehmigungsnotizen:</strong> ${escapeHtml(request.approval_notes)}</p>` : ''}
|
|
${request.rejection_reason ? `<p class="mt-2 text-sm text-red-700 bg-red-50 p-2 rounded"><strong>Ablehnungsgrund:</strong> ${escapeHtml(request.rejection_reason)}</p>` : ''}
|
|
</div>
|
|
|
|
<div class="flex flex-col space-y-2 ml-4">
|
|
<button onclick="showRequestDetails(${request.id})"
|
|
class="btn bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm">
|
|
<i class="fas fa-eye mr-1"></i>Details
|
|
</button>
|
|
|
|
${request.can_be_processed ? `
|
|
<button onclick="approveRequest(${request.id})"
|
|
class="btn bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm">
|
|
<i class="fas fa-check mr-1"></i>Genehmigen
|
|
</button>
|
|
<button onclick="denyRequest(${request.id})"
|
|
class="btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm">
|
|
<i class="fas fa-times mr-1"></i>Ablehnen
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Status-Badge erstellen
|
|
function getStatusBadge(status) {
|
|
const badges = {
|
|
'pending': '<span class="badge badge-pending">Wartend</span>',
|
|
'approved': '<span class="badge badge-approved">Genehmigt</span>',
|
|
'denied': '<span class="badge badge-denied">Abgelehnt</span>'
|
|
};
|
|
return badges[status] || `<span class="badge">${status}</span>`;
|
|
}
|
|
|
|
// Statistiken anzeigen
|
|
function displayStats(stats) {
|
|
const container = document.getElementById('statsContainer');
|
|
container.innerHTML = `
|
|
<span class="stats-badge bg-gray-100 text-gray-800">
|
|
Gesamt: ${stats.total}
|
|
</span>
|
|
<span class="stats-badge bg-yellow-100 text-yellow-800">
|
|
Wartend: ${stats.pending}
|
|
</span>
|
|
<span class="stats-badge bg-green-100 text-green-800">
|
|
Genehmigt: ${stats.approved}
|
|
</span>
|
|
<span class="stats-badge bg-red-100 text-red-800">
|
|
Abgelehnt: ${stats.denied}
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
// Pagination anzeigen
|
|
function displayPagination(pagination) {
|
|
const container = document.getElementById('paginationContainer');
|
|
if (pagination.total <= pagination.limit) {
|
|
container.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const totalPages = Math.ceil(pagination.total / pagination.limit);
|
|
const currentPage = Math.floor(pagination.offset / pagination.limit);
|
|
|
|
let html = '<div class="flex space-x-2">';
|
|
|
|
// Vorherige Seite
|
|
if (currentPage > 0) {
|
|
html += `<button onclick="changePage(${currentPage - 1})" class="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">Zurück</button>`;
|
|
}
|
|
|
|
// Seitenzahlen
|
|
for (let i = Math.max(0, currentPage - 2); i <= Math.min(totalPages - 1, currentPage + 2); i++) {
|
|
const active = i === currentPage ? 'bg-blue-600 text-white' : 'bg-gray-200 hover:bg-gray-300';
|
|
html += `<button onclick="changePage(${i})" class="px-3 py-1 rounded ${active}">${i + 1}</button>`;
|
|
}
|
|
|
|
// Nächste Seite
|
|
if (pagination.has_more) {
|
|
html += `<button onclick="changePage(${currentPage + 1})" class="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">Weiter</button>`;
|
|
}
|
|
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// Verfügbare Drucker laden
|
|
async function loadAvailablePrinters() {
|
|
try {
|
|
const response = await fetch('/api/admin/requests/1'); // Dummy-Request für Drucker-Liste
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.request.available_printers) {
|
|
const select = document.getElementById('printerSelect');
|
|
select.innerHTML = '<option value="">Drucker auswählen...</option>' +
|
|
data.request.available_printers.map(printer =>
|
|
`<option value="${printer.id}">${escapeHtml(printer.name)} (${escapeHtml(printer.location || 'Unbekannt')})</option>`
|
|
).join('');
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Drucker:', error);
|
|
}
|
|
}
|
|
|
|
// Anfrage genehmigen
|
|
function approveRequest(requestId) {
|
|
document.getElementById('actionRequestId').value = requestId;
|
|
document.getElementById('actionType').value = 'approve';
|
|
document.getElementById('actionModalTitle').textContent = 'Anfrage genehmigen';
|
|
document.getElementById('approveSection').classList.remove('hidden');
|
|
document.getElementById('denySection').classList.add('hidden');
|
|
document.getElementById('actionSubmitBtn').textContent = 'Genehmigen';
|
|
document.getElementById('actionSubmitBtn').className = 'px-4 py-2 rounded-md text-white bg-green-600 hover:bg-green-700';
|
|
|
|
// Aktueller Drucker vorauswählen
|
|
const request = currentRequests.find(r => r.id === requestId);
|
|
if (request && request.printer_id) {
|
|
document.getElementById('printerSelect').value = request.printer_id;
|
|
}
|
|
|
|
document.getElementById('actionModal').classList.remove('hidden');
|
|
}
|
|
|
|
// Anfrage ablehnen
|
|
function denyRequest(requestId) {
|
|
document.getElementById('actionRequestId').value = requestId;
|
|
document.getElementById('actionType').value = 'deny';
|
|
document.getElementById('actionModalTitle').textContent = 'Anfrage ablehnen';
|
|
document.getElementById('approveSection').classList.add('hidden');
|
|
document.getElementById('denySection').classList.remove('hidden');
|
|
document.getElementById('actionSubmitBtn').textContent = 'Ablehnen';
|
|
document.getElementById('actionSubmitBtn').className = 'px-4 py-2 rounded-md text-white bg-red-600 hover:bg-red-700';
|
|
|
|
document.getElementById('actionModal').classList.remove('hidden');
|
|
}
|
|
|
|
// Aktions-Form verarbeiten
|
|
async function handleActionSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
const requestId = document.getElementById('actionRequestId').value;
|
|
const actionType = document.getElementById('actionType').value;
|
|
|
|
try {
|
|
let url, data;
|
|
|
|
if (actionType === 'approve') {
|
|
url = `/api/requests/${requestId}/approve`;
|
|
data = {
|
|
printer_id: document.getElementById('printerSelect').value || null,
|
|
notes: document.getElementById('approvalNotes').value
|
|
};
|
|
} else {
|
|
url = `/api/requests/${requestId}/deny`;
|
|
data = {
|
|
reason: document.getElementById('rejectionReason').value
|
|
};
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showSuccess(`Anfrage erfolgreich ${actionType === 'approve' ? 'genehmigt' : 'abgelehnt'}`);
|
|
closeActionModal();
|
|
refreshRequests();
|
|
} else {
|
|
showError('Fehler: ' + result.error);
|
|
}
|
|
} catch (error) {
|
|
showError('Fehler beim Verarbeiten der Anfrage: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Details anzeigen
|
|
async function showRequestDetails(requestId) {
|
|
try {
|
|
const response = await fetch(`/api/admin/requests/${requestId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
displayRequestDetails(data.request);
|
|
document.getElementById('detailModal').classList.remove('hidden');
|
|
} else {
|
|
showError('Fehler beim Laden der Details: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
showError('Fehler beim Laden der Details: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Request-Details anzeigen
|
|
function displayRequestDetails(request) {
|
|
const content = document.getElementById('modalContent');
|
|
content.innerHTML = `
|
|
<div class="space-y-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<h4 class="font-semibold text-gray-800 mb-2">Antragsteller</h4>
|
|
<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>
|
|
<h4 class="font-semibold text-gray-800 mb-2">Anfrage-Details</h4>
|
|
<p><strong>Status:</strong> ${getStatusBadge(request.status)}</p>
|
|
<p><strong>Dauer:</strong> ${request.duration_min} Minuten</p>
|
|
<p><strong>Erstellt:</strong> ${formatDateTime(request.created_at)}</p>
|
|
</div>
|
|
</div>
|
|
|
|
${request.reason ? `
|
|
<div>
|
|
<h4 class="font-semibold text-gray-800 mb-2">Begründung</h4>
|
|
<p class="bg-gray-50 p-3 rounded">${escapeHtml(request.reason)}</p>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div>
|
|
<h4 class="font-semibold text-gray-800 mb-2">Drucker</h4>
|
|
<p>${request.printer ? escapeHtml(request.printer.name) + ' (' + escapeHtml(request.printer.location || 'Unbekannt') + ')' : 'Kein Drucker zugewiesen'}</p>
|
|
</div>
|
|
|
|
${request.processed_by_user ? `
|
|
<div>
|
|
<h4 class="font-semibold text-gray-800 mb-2">Bearbeitung</h4>
|
|
<p><strong>Bearbeitet von:</strong> ${escapeHtml(request.processed_by_user.name)}</p>
|
|
<p><strong>Bearbeitet am:</strong> ${formatDateTime(request.processed_at)}</p>
|
|
</div>
|
|
` : ''}
|
|
|
|
${request.approval_notes ? `
|
|
<div>
|
|
<h4 class="font-semibold text-green-700 mb-2">Genehmigungsnotizen</h4>
|
|
<p class="bg-green-50 p-3 rounded text-green-800">${escapeHtml(request.approval_notes)}</p>
|
|
</div>
|
|
` : ''}
|
|
|
|
${request.rejection_reason ? `
|
|
<div>
|
|
<h4 class="font-semibold text-red-700 mb-2">Ablehnungsgrund</h4>
|
|
<p class="bg-red-50 p-3 rounded text-red-800">${escapeHtml(request.rejection_reason)}</p>
|
|
</div>
|
|
` : ''}
|
|
|
|
${request.job_details ? `
|
|
<div>
|
|
<h4 class="font-semibold text-gray-800 mb-2">Job-Details</h4>
|
|
<p><strong>Job-ID:</strong> ${request.job_details.id}</p>
|
|
<p><strong>Status:</strong> ${escapeHtml(request.job_details.status)}</p>
|
|
<p><strong>Geplanter Start:</strong> ${formatDateTime(request.job_details.start_at)}</p>
|
|
<p><strong>Geplantes Ende:</strong> ${formatDateTime(request.job_details.end_at)}</p>
|
|
${request.job_details.is_overdue ? '<p class="text-red-600"><strong>⚠️ Überfällig</strong></p>' : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Hilfsfunktionen
|
|
function refreshRequests() {
|
|
const status = document.getElementById('statusFilter').value;
|
|
const search = document.getElementById('searchInput').value;
|
|
loadRequests(status, 0, search);
|
|
}
|
|
|
|
function filterRequests() {
|
|
refreshRequests();
|
|
}
|
|
|
|
function debounceSearch() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
refreshRequests();
|
|
}, 500);
|
|
}
|
|
|
|
function changePage(page) {
|
|
const status = document.getElementById('statusFilter').value;
|
|
const search = document.getElementById('searchInput').value;
|
|
loadRequests(status, page * 20, search);
|
|
}
|
|
|
|
function closeDetailModal() {
|
|
document.getElementById('detailModal').classList.add('hidden');
|
|
}
|
|
|
|
function closeActionModal() {
|
|
document.getElementById('actionModal').classList.add('hidden');
|
|
document.getElementById('actionForm').reset();
|
|
}
|
|
|
|
function showLoading(show) {
|
|
document.getElementById('loadingSpinner').classList.toggle('hidden', !show);
|
|
document.getElementById('requestsContainer').classList.toggle('hidden', show);
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
// Einfache Erfolgs-Anzeige
|
|
alert('✅ ' + message);
|
|
}
|
|
|
|
function showError(message) {
|
|
// Einfache Fehler-Anzeige
|
|
alert('❌ ' + message);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return text ? String(text).replace(/[&<>"']/g, function(m) { return 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'
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %} |