restructured admin.js

This commit is contained in:
2026-04-30 16:36:03 +02:00
parent acfc50a244
commit 5bfdda2340

View File

@@ -9,14 +9,10 @@
// =====================================================================
// Block 1: Status Filter for Contributions
// Block 1: Page Tab Navigation
// =====================================================================
let currentFilter = 'all';
// =====================================================================
// Restores active Tab after Page Reload
// =====================================================================
const savedTab = sessionStorage.getItem('admin_active_tab');
if (savedTab) {
// Delays to ensure DOM is ready
@@ -27,48 +23,42 @@ if (savedTab) {
}
// =====================================================================
// Block 3: Page Tab Navigation
// =====================================================================
// Wechselt den sichtbaren Haupt-Tab (Beiträge, Kommentare, Neuigkeiten usw.).
// Speichert den aktiven Tab in sessionStorage für Persistenz nach Reload.
// Page Tab Navigation
function showPageTab(tabName) {
// Saves active Tab for Persistence after Reload
sessionStorage.setItem('admin_active_tab', tabName);
document.querySelectorAll('.page-tab-content').forEach(function (el) {
el.style.display = 'none';
});
// Deactivates all Tab Buttons
document.querySelectorAll('.page-tab').forEach(function (el) {
el.classList.remove('active');
});
// Shows selected Tab and activates Button
document.getElementById('tab-' + tabName).style.display = 'block';
event.currentTarget.classList.add('active');
}
// =====================================================================
// Block 4: Aufklappbare Zeilen (Collapsible Rows)
// Block 2: Collapsible Rows for Contributions and Comments
// =====================================================================
// Öffnet oder schließt eine Beitrags-/Kommentar-Zeile.
// Jeweils nur eine Zeile gleichzeitig geöffnet.
// Lädt bei Bedarf die Karten-Vorschau (Map Preview).
function toggleRow(row) {
const wasOpen = row.classList.contains('open');
// Alle offenen Zeilen schließen
// Closes all open Rows
document.querySelectorAll('.contribution-row.open').forEach(function (el) {
el.classList.remove('open');
});
// Toggles clicked Row
if (!wasOpen) {
row.classList.add('open');
// Karten-Vorschau laden, falls noch nicht geschehen
// Loads Map Preview if not already loaded
const mapDiv = row.querySelector('.detail-map');
if (mapDiv && !mapDiv.dataset.loaded) {
loadMapPreview(mapDiv);
@@ -78,12 +68,8 @@ function toggleRow(row) {
// =====================================================================
// Block 5: Detail-Slider (Karte / Foto)
// Block 3: Details Slider for Maps and Photos
// =====================================================================
// Wechselt zwischen den Slides (Karte und Foto) in einer Beitrags-Zeile.
// direction: -1 für zurück, +1 für vor.
// Wrapped circular: nach dem letzten Slide kommt der erste.
function slideDetail(contributionId, direction) {
const slider = document.getElementById('slider-' + contributionId);
if (!slider) return;
@@ -91,16 +77,19 @@ function slideDetail(contributionId, direction) {
const slides = slider.querySelectorAll('.detail-slide');
let activeIndex = -1;
// Finds active Slide
slides.forEach(function (slide, i) {
if (slide.style.display !== 'none') activeIndex = i;
});
// Calculates next Slide Index
const nextIndex = (activeIndex + direction + slides.length) % slides.length;
// Switches Slides
slides.forEach(function (slide) { slide.style.display = 'none'; });
slides[nextIndex].style.display = 'block';
// Karte beim Zurückwechseln zum Map-Slide laden, falls noch nicht geschehen
// Loads Map if switching to Map Slide
if (slides[nextIndex].dataset.slide === 'map') {
const mapDiv = slides[nextIndex].querySelector('.detail-map');
if (mapDiv && !mapDiv.dataset.loaded) {
@@ -111,7 +100,7 @@ function slideDetail(contributionId, direction) {
// =====================================================================
// Block 6: Leaflet Karten-Vorschau (Mini-Map pro Beitrag)
// Block 4: Map Preview (Leaflet Mini Map per Contribution)
// =====================================================================
// Erstellt eine Leaflet-Mini-Map in einem Beitrags-Detail-Container.
@@ -119,7 +108,8 @@ function slideDetail(contributionId, direction) {
// Markiert die Map als geladen (data-loaded="true"), um doppeltes Laden zu verhindern.
function loadMapPreview(mapDiv) {
const contributionId = mapDiv.dataset.contributionId;
// Fetches all Contributions to find the Geometry
const formData = new FormData();
formData.append('action', 'read');
formData.append('municipality_id', ADMIN_CONFIG.municipalityId);
@@ -129,7 +119,8 @@ function loadMapPreview(mapDiv) {
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data.features) return;
// Finds specific Contribution
const feature = data.features.find(function (f) {
return f.properties.contribution_id == contributionId;
});
@@ -139,6 +130,7 @@ function loadMapPreview(mapDiv) {
return;
}
// Creates Leaflet Mini Map
const miniMap = L.map(mapDiv, {
zoomControl: false,
attributionControl: false,
@@ -150,6 +142,7 @@ function loadMapPreview(mapDiv) {
maxZoom: 20
}).addTo(miniMap);
// Adds Geometry to Mini Map
const geojsonLayer = L.geoJSON(feature, {
style: {
color: ADMIN_CONFIG.primaryColor,
@@ -167,6 +160,7 @@ function loadMapPreview(mapDiv) {
}
}).addTo(miniMap);
// Fits Map to Geometry Bounds
const bounds = geojsonLayer.getBounds();
if (bounds.isValid()) {
miniMap.fitBounds(bounds, { padding: [25, 25], maxZoom: 17 });
@@ -183,19 +177,21 @@ function loadMapPreview(mapDiv) {
// =====================================================================
// Block 7: Beitrags-Filter und Sortierung
// Block 5: Contributions Filter and Sorting
// =====================================================================
// Filtert Beitrags-Zeilen nach Status (pending, approved, rejected, all).
// Aktualisiert die sichtbare Anzahl im Sort-Control.
// Filters Contributions
let currentFilter = 'all';
function filterByStatus(status, tabButton) {
currentFilter = status;
// Updates active Tab
document.querySelectorAll('.filter-tab').forEach(function (el) {
el.classList.remove('active');
});
tabButton.classList.add('active');
// Shows or Hides Contribution Rows
let visibleCount = 0;
document.querySelectorAll('#contributions-container .contribution-row').forEach(function (row) {
if (status === 'all' || row.dataset.status === status) {
@@ -205,11 +201,13 @@ function filterByStatus(status, tabButton) {
row.style.display = 'none';
}
});
// Updates Count Display
document.getElementById('visible-count').textContent = visibleCount + ' Beiträge';
}
// Sortiert Beitrags-Zeilen im DOM nach Datum (auf-/absteigend) oder Kategorie.
// Sorts Contributions
function sortContributions(sortBy) {
const container = document.getElementById('contributions-container');
const rows = Array.from(container.querySelectorAll('.contribution-row'));
@@ -221,23 +219,25 @@ function sortContributions(sortBy) {
return 0;
});
// Reappends sorted Rows
rows.forEach(function (row) { container.appendChild(row); });
}
// =====================================================================
// Block 8: Kommentar-Filter und Sortierung
// Block 6: Comments Filter and Sorting
// =====================================================================
// Filtert Kommentar-Zeilen nach Status (pending, approved, rejected, all).
// Arbeitet ausschließlich auf dem #comment-filter-tabs Container,
// damit keine Kollision mit dem Beitrags-Filter entsteht.
// Filters Comments
function filterCommentsByStatus(status, tabButton) {
// Updates active Tab
document.querySelectorAll('#comment-filter-tabs .filter-tab').forEach(function (el) {
el.classList.remove('active');
});
tabButton.classList.add('active');
// Shows or Hides Comments Rows
let visibleCount = 0;
document.querySelectorAll('.comment-mod-row').forEach(function (row) {
if (status === 'all' || row.dataset.status === status) {
@@ -247,11 +247,12 @@ function filterCommentsByStatus(status, tabButton) {
row.style.display = 'none';
}
});
// Updates Count Display
document.getElementById('comment-visible-count').textContent = visibleCount + ' Kommentare';
}
// Sortiert Kommentar-Zeilen nach Datum oder Beitrags-Titel.
// Sorts Comments
function sortCommentRows(sortBy) {
const container = document.getElementById('comments-mod-container');
const rows = Array.from(container.querySelectorAll('.comment-mod-row'));
@@ -262,49 +263,42 @@ function sortCommentRows(sortBy) {
if (sortBy === 'contribution') return a.dataset.contribution.localeCompare(b.dataset.contribution);
return 0;
});
// Reappends sorted Rows
rows.forEach(function (row) { container.appendChild(row); });
}
// =====================================================================
// Block 9: API-Hilfsfunktion
// Block 7: Helper Functions
// =====================================================================
// Sendet einen POST-Request an die API und gibt ein Promise zurück.
// Analog zu apiCall() in app.js, jedoch Promise-basiert statt Callback-basiert,
// da admin.js keine asynchrone Verkettung mit Callback-Chains benötigt.
// Sends a POST request to API
// promise-based instead of callback-based
function apiCall(data) {
const formData = new FormData();
for (const key in data) {
formData.append(key, data[key]);
}
return fetch(ADMIN_CONFIG.apiUrl, { method: 'POST', body: formData })
.then(function (r) { return r.json(); });
}
// =====================================================================
// Block 10: Hilfsfunktion HTML-Escaping
// =====================================================================
// Verhindert XSS in dynamisch erzeugten SweetAlert-Dialogen.
// Identisch zu escapeHtml() in app.js — beide Dateien arbeiten unabhängig,
// daher keine gemeinsame Abhängigkeit eingeführt.
// Escapes HTML to prevent Cross-Site Scripting (XSS) in Popups and Lists
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
// =====================================================================
// Block 11: Beitrags-Status ändern
// Block 8: CRUD Operations for Contributions
// =====================================================================
// Ändert den Status eines Beitrags (approved / rejected / pending).
// Zeigt eine SweetAlert-Bestätigung und lädt die Seite nach Erfolg neu.
// STATUS: Changes Contribution Status
function changeStatus(contributionId, newStatus) {
const labels = { approved: 'freigeben', rejected: 'ablehnen', pending: 'zurücksetzen' };
@@ -326,18 +320,14 @@ function changeStatus(contributionId, newStatus) {
Swal.fire('Fehler', response.error, 'error');
return;
}
// Reloads Page to reflect Changes
location.reload();
});
});
}
// =====================================================================
// Block 12: Beitrag bearbeiten
// =====================================================================
// Öffnet einen SweetAlert-Dialog zum Bearbeiten von Titel und Beschreibung.
// Werte werden mit escapeHtml() gesichert, bevor sie ins HTML eingefügt werden.
// UPDATE: Edits existing Contributions
function editContribution(contributionId, currentTitle, currentDescription) {
Swal.fire({
title: 'Beitrag bearbeiten',
@@ -382,11 +372,7 @@ function editContribution(contributionId, currentTitle, currentDescription) {
}
// =====================================================================
// Block 13: Beitrag löschen
// =====================================================================
// Zeigt eine Lösch-Bestätigung und entfernt den Beitrag dauerhaft.
// DELETE: Deletes existing Contributions
function deleteContribution(contributionId) {
Swal.fire({
title: 'Beitrag löschen?',
@@ -413,12 +399,11 @@ function deleteContribution(contributionId) {
});
}
// =====================================================================
// Block 14: Kommentar-Status ändern
// Block 9: CRUD Operations for Comments
// =====================================================================
// Ändert den Status eines Kommentars (approved / rejected / pending).
// STATUS: Changes Comment Status
function changeCommentStatus(commentId, newStatus) {
const labels = { approved: 'akzeptieren', rejected: 'ablehnen', pending: 'zurücksetzen' };
@@ -446,11 +431,7 @@ function changeCommentStatus(commentId, newStatus) {
}
// =====================================================================
// Block 15: Kommentar bearbeiten
// =====================================================================
// Öffnet einen Dialog zum Bearbeiten des Kommentar-Inhalts.
// UPDATE: Edits existing Comments
function editModComment(commentId, currentContent) {
Swal.fire({
title: 'Kommentar bearbeiten',
@@ -485,11 +466,7 @@ function editModComment(commentId, currentContent) {
}
// =====================================================================
// Block 16: Kommentar löschen
// =====================================================================
// Zeigt eine Lösch-Bestätigung und entfernt den Kommentar dauerhaft.
// DELETE: Deletes existing Comments
function deleteModComment(commentId) {
Swal.fire({
title: 'Kommentar löschen?',
@@ -518,11 +495,10 @@ function deleteModComment(commentId) {
// =====================================================================
// Block 17: Neuigkeit erstellen
// Block 10: CRUD Operations for News
// =====================================================================
// Öffnet einen Dialog zum Erstellen einer neuen Neuigkeit.
// Titel und Inhalt sind Pflichtfelder, Autor hat einen Standardwert.
// CREATE: Submits new News Article
function createNews() {
Swal.fire({
title: 'Neuigkeit hinzufügen',
@@ -576,11 +552,7 @@ function createNews() {
}
// =====================================================================
// Block 18: Neuigkeit bearbeiten
// =====================================================================
// Öffnet einen Dialog zum Bearbeiten einer bestehenden Neuigkeit.
// UPDATE: Edits existing News
function editNews(newsId, currentTitle, currentContent, currentAuthor) {
Swal.fire({
title: 'Neuigkeit bearbeiten',
@@ -631,13 +603,7 @@ function editNews(newsId, currentTitle, currentContent, currentAuthor) {
}
// =====================================================================
// Block 19: Neuigkeit löschen
// =====================================================================
// Zeigt eine Lösch-Bestätigung und entfernt die Neuigkeit dauerhaft.
// Hinweis: Im Original-Code war dieser Block fälschlicherweise mit
// "// Create News Article" kommentiert — hier korrigiert.
// DELETE: Deletes existing Comments
function deleteNews(newsId) {
Swal.fire({
title: 'Neuigkeit löschen?',