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 // Restores active Tab after Page Reload
// =====================================================================
const savedTab = sessionStorage.getItem('admin_active_tab'); const savedTab = sessionStorage.getItem('admin_active_tab');
if (savedTab) { if (savedTab) {
// Delays to ensure DOM is ready // Delays to ensure DOM is ready
@@ -27,48 +23,42 @@ if (savedTab) {
} }
// ===================================================================== // Page Tab Navigation
// 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.
function showPageTab(tabName) { function showPageTab(tabName) {
// Saves active Tab for Persistence after Reload
sessionStorage.setItem('admin_active_tab', tabName); sessionStorage.setItem('admin_active_tab', tabName);
document.querySelectorAll('.page-tab-content').forEach(function (el) { document.querySelectorAll('.page-tab-content').forEach(function (el) {
el.style.display = 'none'; el.style.display = 'none';
}); });
// Deactivates all Tab Buttons
document.querySelectorAll('.page-tab').forEach(function (el) { document.querySelectorAll('.page-tab').forEach(function (el) {
el.classList.remove('active'); el.classList.remove('active');
}); });
// Shows selected Tab and activates Button
document.getElementById('tab-' + tabName).style.display = 'block'; document.getElementById('tab-' + tabName).style.display = 'block';
event.currentTarget.classList.add('active'); 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) { function toggleRow(row) {
const wasOpen = row.classList.contains('open'); const wasOpen = row.classList.contains('open');
// Alle offenen Zeilen schließen // Closes all open Rows
document.querySelectorAll('.contribution-row.open').forEach(function (el) { document.querySelectorAll('.contribution-row.open').forEach(function (el) {
el.classList.remove('open'); el.classList.remove('open');
}); });
// Toggles clicked Row
if (!wasOpen) { if (!wasOpen) {
row.classList.add('open'); row.classList.add('open');
// Karten-Vorschau laden, falls noch nicht geschehen // Loads Map Preview if not already loaded
const mapDiv = row.querySelector('.detail-map'); const mapDiv = row.querySelector('.detail-map');
if (mapDiv && !mapDiv.dataset.loaded) { if (mapDiv && !mapDiv.dataset.loaded) {
loadMapPreview(mapDiv); 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) { function slideDetail(contributionId, direction) {
const slider = document.getElementById('slider-' + contributionId); const slider = document.getElementById('slider-' + contributionId);
if (!slider) return; if (!slider) return;
@@ -91,16 +77,19 @@ function slideDetail(contributionId, direction) {
const slides = slider.querySelectorAll('.detail-slide'); const slides = slider.querySelectorAll('.detail-slide');
let activeIndex = -1; let activeIndex = -1;
// Finds active Slide
slides.forEach(function (slide, i) { slides.forEach(function (slide, i) {
if (slide.style.display !== 'none') activeIndex = i; if (slide.style.display !== 'none') activeIndex = i;
}); });
// Calculates next Slide Index
const nextIndex = (activeIndex + direction + slides.length) % slides.length; const nextIndex = (activeIndex + direction + slides.length) % slides.length;
// Switches Slides
slides.forEach(function (slide) { slide.style.display = 'none'; }); slides.forEach(function (slide) { slide.style.display = 'none'; });
slides[nextIndex].style.display = 'block'; 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') { if (slides[nextIndex].dataset.slide === 'map') {
const mapDiv = slides[nextIndex].querySelector('.detail-map'); const mapDiv = slides[nextIndex].querySelector('.detail-map');
if (mapDiv && !mapDiv.dataset.loaded) { 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. // 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. // Markiert die Map als geladen (data-loaded="true"), um doppeltes Laden zu verhindern.
function loadMapPreview(mapDiv) { function loadMapPreview(mapDiv) {
const contributionId = mapDiv.dataset.contributionId; const contributionId = mapDiv.dataset.contributionId;
// Fetches all Contributions to find the Geometry
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'read'); formData.append('action', 'read');
formData.append('municipality_id', ADMIN_CONFIG.municipalityId); formData.append('municipality_id', ADMIN_CONFIG.municipalityId);
@@ -129,7 +119,8 @@ function loadMapPreview(mapDiv) {
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function (data) { .then(function (data) {
if (!data.features) return; if (!data.features) return;
// Finds specific Contribution
const feature = data.features.find(function (f) { const feature = data.features.find(function (f) {
return f.properties.contribution_id == contributionId; return f.properties.contribution_id == contributionId;
}); });
@@ -139,6 +130,7 @@ function loadMapPreview(mapDiv) {
return; return;
} }
// Creates Leaflet Mini Map
const miniMap = L.map(mapDiv, { const miniMap = L.map(mapDiv, {
zoomControl: false, zoomControl: false,
attributionControl: false, attributionControl: false,
@@ -150,6 +142,7 @@ function loadMapPreview(mapDiv) {
maxZoom: 20 maxZoom: 20
}).addTo(miniMap); }).addTo(miniMap);
// Adds Geometry to Mini Map
const geojsonLayer = L.geoJSON(feature, { const geojsonLayer = L.geoJSON(feature, {
style: { style: {
color: ADMIN_CONFIG.primaryColor, color: ADMIN_CONFIG.primaryColor,
@@ -167,6 +160,7 @@ function loadMapPreview(mapDiv) {
} }
}).addTo(miniMap); }).addTo(miniMap);
// Fits Map to Geometry Bounds
const bounds = geojsonLayer.getBounds(); const bounds = geojsonLayer.getBounds();
if (bounds.isValid()) { if (bounds.isValid()) {
miniMap.fitBounds(bounds, { padding: [25, 25], maxZoom: 17 }); 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). // Filters Contributions
// Aktualisiert die sichtbare Anzahl im Sort-Control. let currentFilter = 'all';
function filterByStatus(status, tabButton) { function filterByStatus(status, tabButton) {
currentFilter = status; currentFilter = status;
// Updates active Tab
document.querySelectorAll('.filter-tab').forEach(function (el) { document.querySelectorAll('.filter-tab').forEach(function (el) {
el.classList.remove('active'); el.classList.remove('active');
}); });
tabButton.classList.add('active'); tabButton.classList.add('active');
// Shows or Hides Contribution Rows
let visibleCount = 0; let visibleCount = 0;
document.querySelectorAll('#contributions-container .contribution-row').forEach(function (row) { document.querySelectorAll('#contributions-container .contribution-row').forEach(function (row) {
if (status === 'all' || row.dataset.status === status) { if (status === 'all' || row.dataset.status === status) {
@@ -205,11 +201,13 @@ function filterByStatus(status, tabButton) {
row.style.display = 'none'; row.style.display = 'none';
} }
}); });
// Updates Count Display
document.getElementById('visible-count').textContent = visibleCount + ' Beiträge'; document.getElementById('visible-count').textContent = visibleCount + ' Beiträge';
} }
// Sortiert Beitrags-Zeilen im DOM nach Datum (auf-/absteigend) oder Kategorie.
// Sorts Contributions
function sortContributions(sortBy) { function sortContributions(sortBy) {
const container = document.getElementById('contributions-container'); const container = document.getElementById('contributions-container');
const rows = Array.from(container.querySelectorAll('.contribution-row')); const rows = Array.from(container.querySelectorAll('.contribution-row'));
@@ -221,23 +219,25 @@ function sortContributions(sortBy) {
return 0; return 0;
}); });
// Reappends sorted Rows
rows.forEach(function (row) { container.appendChild(row); }); 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). // Filters Comments
// Arbeitet ausschließlich auf dem #comment-filter-tabs Container,
// damit keine Kollision mit dem Beitrags-Filter entsteht.
function filterCommentsByStatus(status, tabButton) { function filterCommentsByStatus(status, tabButton) {
// Updates active Tab
document.querySelectorAll('#comment-filter-tabs .filter-tab').forEach(function (el) { document.querySelectorAll('#comment-filter-tabs .filter-tab').forEach(function (el) {
el.classList.remove('active'); el.classList.remove('active');
}); });
tabButton.classList.add('active'); tabButton.classList.add('active');
// Shows or Hides Comments Rows
let visibleCount = 0; let visibleCount = 0;
document.querySelectorAll('.comment-mod-row').forEach(function (row) { document.querySelectorAll('.comment-mod-row').forEach(function (row) {
if (status === 'all' || row.dataset.status === status) { if (status === 'all' || row.dataset.status === status) {
@@ -247,11 +247,12 @@ function filterCommentsByStatus(status, tabButton) {
row.style.display = 'none'; row.style.display = 'none';
} }
}); });
// Updates Count Display
document.getElementById('comment-visible-count').textContent = visibleCount + ' Kommentare'; document.getElementById('comment-visible-count').textContent = visibleCount + ' Kommentare';
} }
// Sortiert Kommentar-Zeilen nach Datum oder Beitrags-Titel.
// Sorts Comments
function sortCommentRows(sortBy) { function sortCommentRows(sortBy) {
const container = document.getElementById('comments-mod-container'); const container = document.getElementById('comments-mod-container');
const rows = Array.from(container.querySelectorAll('.comment-mod-row')); 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); if (sortBy === 'contribution') return a.dataset.contribution.localeCompare(b.dataset.contribution);
return 0; return 0;
}); });
// Reappends sorted Rows
rows.forEach(function (row) { container.appendChild(row); }); 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. // Sends a POST request to API
// Analog zu apiCall() in app.js, jedoch Promise-basiert statt Callback-basiert, // promise-based instead of callback-based
// da admin.js keine asynchrone Verkettung mit Callback-Chains benötigt.
function apiCall(data) { function apiCall(data) {
const formData = new FormData(); const formData = new FormData();
for (const key in data) { for (const key in data) {
formData.append(key, data[key]); formData.append(key, data[key]);
} }
return fetch(ADMIN_CONFIG.apiUrl, { method: 'POST', body: formData }) return fetch(ADMIN_CONFIG.apiUrl, { method: 'POST', body: formData })
.then(function (r) { return r.json(); }); .then(function (r) { return r.json(); });
} }
// ===================================================================== // Escapes HTML to prevent Cross-Site Scripting (XSS) in Popups and Lists
// 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.
function escapeHtml(text) { function escapeHtml(text) {
if (!text) return ''; if (!text) return '';
const div = document.createElement('div'); const div = document.createElement('div');
div.appendChild(document.createTextNode(text)); div.appendChild(document.createTextNode(text));
return div.innerHTML; return div.innerHTML;
} }
// ===================================================================== // =====================================================================
// Block 11: Beitrags-Status ändern // Block 8: CRUD Operations for Contributions
// ===================================================================== // =====================================================================
// Ändert den Status eines Beitrags (approved / rejected / pending). // STATUS: Changes Contribution Status
// Zeigt eine SweetAlert-Bestätigung und lädt die Seite nach Erfolg neu.
function changeStatus(contributionId, newStatus) { function changeStatus(contributionId, newStatus) {
const labels = { approved: 'freigeben', rejected: 'ablehnen', pending: 'zurücksetzen' }; const labels = { approved: 'freigeben', rejected: 'ablehnen', pending: 'zurücksetzen' };
@@ -326,18 +320,14 @@ function changeStatus(contributionId, newStatus) {
Swal.fire('Fehler', response.error, 'error'); Swal.fire('Fehler', response.error, 'error');
return; return;
} }
// Reloads Page to reflect Changes
location.reload(); location.reload();
}); });
}); });
} }
// ===================================================================== // UPDATE: Edits existing Contributions
// 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.
function editContribution(contributionId, currentTitle, currentDescription) { function editContribution(contributionId, currentTitle, currentDescription) {
Swal.fire({ Swal.fire({
title: 'Beitrag bearbeiten', title: 'Beitrag bearbeiten',
@@ -382,11 +372,7 @@ function editContribution(contributionId, currentTitle, currentDescription) {
} }
// ===================================================================== // DELETE: Deletes existing Contributions
// Block 13: Beitrag löschen
// =====================================================================
// Zeigt eine Lösch-Bestätigung und entfernt den Beitrag dauerhaft.
function deleteContribution(contributionId) { function deleteContribution(contributionId) {
Swal.fire({ Swal.fire({
title: 'Beitrag löschen?', 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) { function changeCommentStatus(commentId, newStatus) {
const labels = { approved: 'akzeptieren', rejected: 'ablehnen', pending: 'zurücksetzen' }; const labels = { approved: 'akzeptieren', rejected: 'ablehnen', pending: 'zurücksetzen' };
@@ -446,11 +431,7 @@ function changeCommentStatus(commentId, newStatus) {
} }
// ===================================================================== // UPDATE: Edits existing Comments
// Block 15: Kommentar bearbeiten
// =====================================================================
// Öffnet einen Dialog zum Bearbeiten des Kommentar-Inhalts.
function editModComment(commentId, currentContent) { function editModComment(commentId, currentContent) {
Swal.fire({ Swal.fire({
title: 'Kommentar bearbeiten', title: 'Kommentar bearbeiten',
@@ -485,11 +466,7 @@ function editModComment(commentId, currentContent) {
} }
// ===================================================================== // DELETE: Deletes existing Comments
// Block 16: Kommentar löschen
// =====================================================================
// Zeigt eine Lösch-Bestätigung und entfernt den Kommentar dauerhaft.
function deleteModComment(commentId) { function deleteModComment(commentId) {
Swal.fire({ Swal.fire({
title: 'Kommentar löschen?', 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. // CREATE: Submits new News Article
// Titel und Inhalt sind Pflichtfelder, Autor hat einen Standardwert.
function createNews() { function createNews() {
Swal.fire({ Swal.fire({
title: 'Neuigkeit hinzufügen', title: 'Neuigkeit hinzufügen',
@@ -576,11 +552,7 @@ function createNews() {
} }
// ===================================================================== // UPDATE: Edits existing News
// Block 18: Neuigkeit bearbeiten
// =====================================================================
// Öffnet einen Dialog zum Bearbeiten einer bestehenden Neuigkeit.
function editNews(newsId, currentTitle, currentContent, currentAuthor) { function editNews(newsId, currentTitle, currentContent, currentAuthor) {
Swal.fire({ Swal.fire({
title: 'Neuigkeit bearbeiten', title: 'Neuigkeit bearbeiten',
@@ -631,13 +603,7 @@ function editNews(newsId, currentTitle, currentContent, currentAuthor) {
} }
// ===================================================================== // DELETE: Deletes existing Comments
// 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.
function deleteNews(newsId) { function deleteNews(newsId) {
Swal.fire({ Swal.fire({
title: 'Neuigkeit löschen?', title: 'Neuigkeit löschen?',