FluxInator / templates /backend.html
Scalino84
Initial commit
1e7308f
raw
history blame
22.5 kB
{% 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 %}