Spaces:
Runtime error
Runtime error
{% extends "base.html" %} | |
{% block title %}Backend Verwaltung{% endblock %} | |
{% block head %} | |
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> | |
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> | |
<script src="https://unpkg.com/recharts/umd/Recharts.min.js"></script> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" rel="stylesheet"> | |
<style> | |
.hoverable-row:hover { | |
background-color: rgba(0,0,0,0.05); | |
cursor: pointer; | |
} | |
.card { | |
transition: transform 0.2s; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
.card:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.15); | |
} | |
.stat-card { | |
border-radius: 10px; | |
border: none; | |
background: linear-gradient(45deg, #3498db, #2980b9); | |
color: white; | |
} | |
.stat-icon { | |
font-size: 2rem; | |
margin-bottom: 1rem; | |
} | |
.stat-value { | |
font-size: 1.5rem; | |
font-weight: bold; | |
} | |
.stat-label { | |
font-size: 0.9rem; | |
opacity: 0.9; | |
} | |
.action-button { | |
transition: all 0.2s; | |
} | |
.action-button:hover { | |
transform: scale(1.05); | |
} | |
.toast-container { | |
z-index: 1051; | |
} | |
.chart-container { | |
position: relative; | |
margin: 20px 0; | |
height: 300px; | |
} | |
.loader { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
} | |
</style> | |
{% endblock %} | |
{% block content %} | |
<form id="generateForm" class="needs-validation custom-bg p-3 rounded" novalidate> | |
<div class="container-fluid py-4"> | |
<div class="d-flex justify-content-between align-items-center mb-4"> | |
<div class="btn-group"> | |
<button class="btn btn-primary" onclick="refreshData()"> | |
<i class="bi bi-arrow-clockwise"></i> Aktualisieren | |
</button> | |
<button class="btn btn-secondary" onclick="toggleDarkMode()"> | |
<i class="bi bi-moon"></i> Dark Mode | |
</button> | |
</div> | |
</div> | |
<!-- Quick Stats Row --> | |
<div class="row mb-4"> | |
<div class="col-md-3"> | |
<div class="card stat-card mb-3"> | |
<div class="card-body text-center"> | |
<i class="bi bi-images stat-icon"></i> | |
<div class="stat-value" id="totalImages">0</div> | |
<div class="stat-label">Gesamt Bilder</div> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-3"> | |
<div class="card stat-card mb-3"> | |
<div class="card-body text-center"> | |
<i class="bi bi-folder stat-icon"></i> | |
<div class="stat-value" id="totalAlbums">0</div> | |
<div class="stat-label">Alben</div> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-3"> | |
<div class="card stat-card mb-3"> | |
<div class="card-body text-center"> | |
<i class="bi bi-tags stat-icon"></i> | |
<div class="stat-value" id="totalCategories">0</div> | |
<div class="stat-label">Kategorien</div> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-3"> | |
<div class="card stat-card mb-3"> | |
<div class="card-body text-center"> | |
<i class="bi bi-hdd stat-icon"></i> | |
<div class="stat-value" id="storageUsage">0 MB</div> | |
<div class="stat-label">Speicherplatz</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Main Content --> | |
<div class="row"> | |
<!-- Album Management --> | |
<div id="albumStats" class="col-12 col-lg-6 mb-4"> | |
<div class="card h-100"> | |
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center"> | |
<h5 class="mb-0">Album Verwaltung</h5> | |
<button class="btn btn-light btn-sm" onclick="showNewAlbumModal()"> | |
<i class="bi bi-plus-lg"></i> Neu | |
</button> | |
</div> | |
<div class="card-body"> | |
<div class="table-responsive"> | |
<table class="table table-hover" id="albumTable"> | |
<thead> | |
<tr> | |
<th> | |
<input type="checkbox" class="form-check-input" id="selectAllAlbums"> | |
</th> | |
<th>Name</th> | |
<th>Bilder</th> | |
<th>Erstellt</th> | |
<th>Aktionen</th> | |
</tr> | |
</thead> | |
<tr> | |
<td> | |
<div class="album-list d-flex flex-wrap"> | |
{% for album in albums %} | |
<div class="album-item p-2"> | |
{{ album[1] }} | |
</div> | |
{% endfor %} | |
</div> | |
</td> | |
</tr> | |
</table> | |
</div> | |
<div id="albumPagination" class="d-flex justify-content-between align-items-center mt-3"> | |
<div class="d-flex align-items-center"> | |
<select class="form-select me-2" id="albumPageSize"> | |
<option value="10">10</option> | |
<option value="25">25</option> | |
<option value="50">50</option> | |
</select> | |
<span>Einträge pro Seite</span> | |
</div> | |
<nav> | |
<ul class="pagination mb-0"> | |
<!-- Pagination will be inserted here --> | |
</ul> | |
</nav> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Category Management --> | |
<div id="categoryStats" class="col-12 col-lg-6 mb-4"> | |
<div class="card h-100"> | |
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center"> | |
<h5 class="mb-0">Kategorie Verwaltung</h5> | |
<button class="btn btn-light btn-sm" onclick="showNewCategoryModal()"> | |
<i class="bi bi-plus-lg"></i> Neu | |
</button> | |
</div> | |
<div class="card-body"> | |
<div class="table-responsive"> | |
<table class="table table-hover" id="categoryTable"> | |
<thead> | |
<tr> | |
<th> | |
<input type="checkbox" class="form-check-input" id="selectAllCategories"> | |
</th> | |
<th>Name</th> | |
<th>Bilder</th> | |
<th>Erstellt</th> | |
<th>Aktionen</th> | |
</tr> | |
</thead> | |
<tbody id="categoryList"> | |
<!-- Category rows will be inserted here --> | |
</tbody> | |
</table> | |
</div> | |
<div id="categoryPagination" class="d-flex justify-content-between align-items-center mt-3"> | |
<div class="d-flex align-items-center"> | |
<select class="form-select me-2" id="categoryPageSize"> | |
<option value="10">10</option> | |
<option value="25">25</option> | |
<option value="50">50</option> | |
</select> | |
<span>Einträge pro Seite</span> | |
</div> | |
<nav> | |
<ul class="pagination mb-0"> | |
<!-- Pagination will be inserted here --> | |
</ul> | |
</nav> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</form> | |
<!-- Statistics Row --> | |
<!-- <div class="row"> | |
<div class="col-12"> | |
<div class="card"> | |
<div class="card-header bg-primary text-white"> | |
<h5 class="mb-0">Statistiken</h5> | |
</div> | |
<div class="card-body"> | |
<div class="row"> | |
<div class="col-md-6"> | |
<div class="chart-container"> | |
<canvas id="monthlyStats"></canvas> | |
<div class="loader" id="monthlyStatsLoader"> | |
<div class="spinner-border text-primary" role="status"> | |
<span class="visually-hidden">Laden...</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-6"> | |
<div class="chart-container"> | |
<canvas id="categoryStats"></canvas> | |
<div class="loader" id="categoryStatsLoader"> | |
<div class="spinner-border text-primary" role="status"> | |
<span class="visually-hidden">Laden...</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="row mt-4"> | |
<div class="col-md-12"> | |
<div class="chart-container"> | |
<canvas id="storageStats"></canvas> | |
<div class="loader" id="storageStatsLoader"> | |
<div class="spinner-border text-primary" role="status"> | |
<span class="visually-hidden">Laden...</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> --> | |
<!-- Modals --> | |
<!-- New Album Modal --> | |
<div class="modal fade" id="newAlbumModal" tabindex="-1"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Neues Album erstellen</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<form id="newAlbumForm"> | |
<div class="mb-3"> | |
<label for="albumName" class="form-label">Album Name</label> | |
<input type="text" class="form-control" id="albumName" required> | |
</div> | |
<div class="mb-3"> | |
<label for="albumDescription" class="form-label">Beschreibung (optional)</label> | |
<textarea class="form-control" id="albumDescription" rows="3"></textarea> | |
</div> | |
</form> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> | |
<button type="button" class="btn btn-primary" onclick="createAlbum()">Erstellen</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- New Category Modal --> | |
<div class="modal fade" id="newCategoryModal" tabindex="-1"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Neue Kategorie erstellen</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<form id="newCategoryForm"> | |
<div class="mb-3"> | |
<label for="categoryName" class="form-label">Kategorie Name</label> | |
<input type="text" class="form-control" id="categoryName" required> | |
</div> | |
<div class="mb-3"> | |
<label for="categoryDescription" class="form-label">Beschreibung (optional)</label> | |
<textarea class="form-control" id="categoryDescription" rows="3"></textarea> | |
</div> | |
</form> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> | |
<button type="button" class="btn btn-primary" onclick="createCategory()">Erstellen</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Delete Confirmation Modal --> | |
<div class="modal fade" id="deleteModal" tabindex="-1"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Löschen bestätigen</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<p>Möchten Sie <span id="deleteItemName"></span> wirklich löschen?</p> | |
<p class="text-danger">Diese Aktion kann nicht rückgängig gemacht werden!</p> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> | |
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Löschen</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Bulk Action Modal --> | |
<div class="modal fade" id="bulkActionModal" tabindex="-1"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Massenbearbeitung</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<p>Ausgewählte Elemente: <span id="selectedCount">0</span></p> | |
<div class="list-group"> | |
<button type="button" class="list-group-item list-group-item-action" onclick="bulkDelete()"> | |
<i class="bi bi-trash"></i> Alle löschen | |
</button> | |
<button type="button" class="list-group-item list-group-item-action" onclick="mergeSelected()"> | |
<i class="bi bi-arrow-join"></i> Zusammenführen | |
</button> | |
<button type="button" class="list-group-item list-group-item-action" onclick="exportSelected()"> | |
<i class="bi bi-download"></i> Exportieren | |
</button> | |
</div> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Toast Container --> | |
<!-- <div class="toast-container position-fixed bottom-0 end-0 p-3"> | |
<div id="toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true"> | |
<div class="toast-header"> | |
<i class="bi bi-info-circle me-2"></i> | |
<strong class="me-auto" id="toastTitle"></strong> | |
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> | |
</div> | |
<div class="toast-body" id="toastMessage"></div> | |
</div> | |
</div> --> | |
{% endblock %} | |
{% block scripts %} | |
<!-- Chart.js für Statistiken --> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<!-- Backend Manager Script --> | |
<script src="/static/backend.js"></script> | |
<!-- React Components --> | |
<script type="module"> | |
import AlbumManager from '/static/components/AlbumManager.js'; | |
import CategoryManager from '/static/components/CategoryManager.js'; | |
import StatsVisualization from '/static/components/StatsVisualization.js'; | |
// React Components initialisieren | |
ReactDOM.createRoot(document.getElementById('albumManagerRoot')).render( | |
React.createElement(AlbumManager) | |
); | |
ReactDOM.createRoot(document.getElementById('categoryManagerRoot')).render( | |
React.createElement(CategoryManager) | |
); | |
ReactDOM.createRoot(document.getElementById('statsRoot')).render( | |
React.createElement(StatsVisualization) | |
); | |
</script> | |
<!-- Inline Script für zusätzliche Funktionalität --> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Dark Mode Toggle | |
const darkModeToggle = document.getElementById('darkModeToggle'); | |
if (darkModeToggle) { | |
darkModeToggle.addEventListener('click', function() { | |
document.body.classList.toggle('dark-mode'); | |
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode')); | |
}); | |
// Dark Mode aus localStorage wiederherstellen | |
if (localStorage.getItem('darkMode') === 'true') { | |
document.body.classList.add('dark-mode'); | |
} | |
} | |
// Toast Funktionalität | |
window.showToast = function(title, message, type = 'info') { | |
const toast = document.getElementById('toast'); | |
const toastTitle = document.getElementById('toastTitle'); | |
const toastMessage = document.getElementById('toastMessage'); | |
toast.classList.remove('bg-success', 'bg-danger', 'bg-info'); | |
toast.classList.add(`bg-${type}`); | |
toastTitle.textContent = title; | |
toastMessage.textContent = message; | |
const bsToast = new bootstrap.Toast(toast); | |
bsToast.show(); | |
}; | |
// Pagination Event Listener | |
document.querySelectorAll('.page-link').forEach(button => { | |
button.addEventListener('click', function(e) { | |
e.preventDefault(); | |
const page = this.getAttribute('data-page'); | |
loadPage(page); | |
}); | |
}); | |
// Items per Page Event Listener | |
document.querySelectorAll('.items-per-page').forEach(select => { | |
select.addEventListener('change', function() { | |
loadPage(1); | |
}); | |
}); | |
// Select All Checkboxes | |
document.querySelectorAll('.select-all').forEach(checkbox => { | |
checkbox.addEventListener('change', function() { | |
const itemCheckboxes = document.querySelectorAll( | |
`.item-checkbox[data-type="${this.dataset.type}"]` | |
); | |
itemCheckboxes.forEach(item => item.checked = this.checked); | |
updateBulkActionButton(); | |
}); | |
}); | |
// Bulk Action Button Update | |
function updateBulkActionButton() { | |
const selectedCount = document.querySelectorAll('.item-checkbox:checked').length; | |
const bulkActionBtn = document.getElementById('bulkActionBtn'); | |
if (bulkActionBtn) { | |
bulkActionBtn.disabled = selectedCount === 0; | |
document.getElementById('selectedCount').textContent = selectedCount; | |
} | |
} | |
// Initial Load | |
loadPage(1); | |
updateStatistics(); | |
}); | |
// Refresh Funktionen | |
async function refreshData() { | |
try { | |
await Promise.all([ | |
loadPage(1), | |
updateStatistics() | |
]); | |
showToast('Erfolg', 'Daten erfolgreich aktualisiert', 'success'); | |
} catch (error) { | |
showToast('Fehler', 'Fehler beim Aktualisieren der Daten', 'danger'); | |
} | |
} | |
// Statistik Aktualisierung | |
async function updateStatistics() { | |
try { | |
const response = await fetch('/backend/stats'); | |
const stats = await response.json(); | |
// Update Quick Stats | |
document.getElementById('totalImages').textContent = stats.total_images; | |
document.getElementById('totalAlbums').textContent = stats.albums.total; | |
document.getElementById('totalCategories').textContent = stats.categories.total; | |
document.getElementById('storageUsage').textContent = | |
`${Math.round(stats.storage_usage_mb * 100) / 100} MB`; | |
// Update Charts | |
updateCharts(stats); | |
} catch (error) { | |
console.error('Fehler beim Laden der Statistiken:', error); | |
} | |
} | |
// Chart Aktualisierung | |
function updateCharts(stats) { | |
updateMonthlyChart(stats.monthly); | |
updateCategoryChart(stats.categories); | |
updateStorageChart(stats.storage); | |
} | |
function createChart(ctx, config) { | |
if (window.charts && window.charts[ctx.id]) { | |
window.charts[ctx.id].destroy(); | |
} | |
window.charts = window.charts || {}; | |
window.charts[ctx.id] = new Chart(ctx, config); | |
} | |
// Export Funktion | |
async function exportSelected() { | |
const selectedIds = Array.from(document.querySelectorAll('.item-checkbox:checked')) | |
.map(cb => cb.dataset.id); | |
if (selectedIds.length === 0) { | |
showToast('Warnung', 'Keine Elemente ausgewählt', 'warning'); | |
return; | |
} | |
try { | |
const response = await fetch('/backend/export', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ ids: selectedIds }) | |
}); | |
if (!response.ok) throw new Error('Export fehlgeschlagen'); | |
const blob = await response.blob(); | |
const url = window.URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = 'export.csv'; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
window.URL.revokeObjectURL(url); | |
showToast('Erfolg', 'Export erfolgreich', 'success'); | |
} catch (error) { | |
showToast('Fehler', 'Export fehlgeschlagen', 'danger'); | |
} | |
} | |
// Error Handler | |
function handleError(error) { | |
console.error('Fehler:', error); | |
showToast('Fehler', error.message || 'Ein Fehler ist aufgetreten', 'danger'); | |
} | |
</script> | |
{% endblock %} | |