Projektarbeit-MYP/backend/app/templates/admin_guest_requests.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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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 %}