// ===================================================================== // WebGIS Moderation Portal — Application Logic // Initializes Map Preview, loads Contributions from the API, // handles CRUD Workflow, sorting and filtering for Contributions, // Comments and News, and manages all UI Interactions // // Depends on: ADMIN_CONFIG Object set in Moderation Page // ===================================================================== // ===================================================================== // Block 0: Configuration and Application State // ===================================================================== // API Endpoint as relative Path const API_URL = 'api/contributions.php'; // ===================================================================== // Block 1: Page Tab Navigation // ===================================================================== // Restores active Tab after Page Reload const savedTab = sessionStorage.getItem('admin_active_tab'); if (savedTab) { // Delays to ensure DOM is ready setTimeout(function () { const tabBtn = document.querySelector('.page-tab[onclick*="' + savedTab + '"]'); if (tabBtn) tabBtn.click(); }, 100); } // 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 2: Collapsible Rows for Contributions and Comments // ===================================================================== function toggleRow(row) { const wasOpen = row.classList.contains('open'); // 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'); // Loads Map Preview if not already loaded const mapDiv = row.querySelector('.detail-map'); if (mapDiv && !mapDiv.dataset.loaded) { loadMapPreview(mapDiv); } } } // ===================================================================== // Block 3: Details Slider for Maps and Photos // ===================================================================== function slideDetail(contributionId, direction) { const slider = document.getElementById('slider-' + contributionId); if (!slider) return; 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'; // 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) { loadMapPreview(mapDiv); } } } // ===================================================================== // Block 4: Map Preview (Leaflet Mini Map per Contribution) // ===================================================================== // Erstellt eine Leaflet-Mini-Map in einem Beitrags-Detail-Container. // Lädt alle Beiträge via API und zeigt die Geometrie des entsprechenden Beitrags. // 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.id); formData.append('status', 'all'); fetch(API_URL, { method: 'POST', body: formData }) .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; }); if (!feature) { mapDiv.innerHTML = '
Geometrie nicht gefunden.
'; return; } // Creates Leaflet Mini Map const miniMap = L.map(mapDiv, { zoomControl: false, attributionControl: false, dragging: true, scrollWheelZoom: false }); L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { maxZoom: 20 }).addTo(miniMap); // Adds Geometry to Mini Map const geojsonLayer = L.geoJSON(feature, { style: { color: ADMIN_CONFIG.primaryColor, weight: 3, fillOpacity: 0.2 }, pointToLayer: function (f, latlng) { return L.circleMarker(latlng, { radius: 8, color: '#ffffff', weight: 2, fillColor: ADMIN_CONFIG.primaryColor, fillOpacity: 0.9 }); } }).addTo(miniMap); // Fits Map to Geometry Bounds const bounds = geojsonLayer.getBounds(); if (bounds.isValid()) { miniMap.fitBounds(bounds, { padding: [25, 25], maxZoom: 17 }); } else { miniMap.setView(ADMIN_CONFIG.center, 15); } mapDiv.dataset.loaded = 'true'; }) .catch(function () { mapDiv.innerHTML = '
Karte nicht verfügbar.
'; }); } // ===================================================================== // Block 5: Contributions Filter and Sorting // ===================================================================== // 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) { row.style.display = ''; visibleCount++; } else { row.style.display = 'none'; } }); // Updates Count Display document.getElementById('visible-count').textContent = visibleCount + ' Beiträge'; } // Sorts Contributions function sortContributions(sortBy) { const container = document.getElementById('contributions-container'); const rows = Array.from(container.querySelectorAll('.contribution-row')); rows.sort(function (a, b) { if (sortBy === 'date-desc') return new Date(b.dataset.date) - new Date(a.dataset.date); if (sortBy === 'date-asc') return new Date(a.dataset.date) - new Date(b.dataset.date); if (sortBy === 'category') return a.dataset.category.localeCompare(b.dataset.category); return 0; }); // Reappends sorted Rows rows.forEach(function (row) { container.appendChild(row); }); } // ===================================================================== // Block 6: Comments Filter and Sorting // ===================================================================== // 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) { row.style.display = ''; visibleCount++; } else { row.style.display = 'none'; } }); // Updates Count Display document.getElementById('comment-visible-count').textContent = visibleCount + ' Kommentare'; } // Sorts Comments function sortCommentRows(sortBy) { const container = document.getElementById('comments-mod-container'); const rows = Array.from(container.querySelectorAll('.comment-mod-row')); rows.sort(function (a, b) { if (sortBy === 'date-desc') return new Date(b.dataset.date) - new Date(a.dataset.date); if (sortBy === 'date-asc') return new Date(a.dataset.date) - new Date(b.dataset.date); if (sortBy === 'contribution') return a.dataset.contribution.localeCompare(b.dataset.contribution); return 0; }); // Reappends sorted Rows rows.forEach(function (row) { container.appendChild(row); }); } // ===================================================================== // Block 7: Helper Functions // ===================================================================== // 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(API_URL, { method: 'POST', body: formData }) .then(function (r) { return r.json(); }); } // 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 8: CRUD Operations for Contributions // ===================================================================== // STATUS: Changes Contribution Status function changeStatus(contributionId, newStatus) { const labels = { approved: 'freigeben', rejected: 'ablehnen', pending: 'zurücksetzen' }; Swal.fire({ title: 'Beitrag ' + labels[newStatus] + '?', showCancelButton: true, confirmButtonText: 'Ja', cancelButtonText: 'Abbrechen', confirmButtonColor: ADMIN_CONFIG.primaryColor }).then(function (result) { if (!result.isConfirmed) return; apiCall({ action: 'update', contribution_id: contributionId, status: newStatus }).then(function (response) { if (response.error) { Swal.fire('Fehler', response.error, 'error'); return; } // Reloads Page to reflect Changes location.reload(); }); }); } // UPDATE: Edits existing Contributions function editContribution(contributionId, currentTitle, currentDescription) { Swal.fire({ title: 'Beitrag bearbeiten', html: '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
', showCancelButton: true, confirmButtonText: 'Speichern', cancelButtonText: 'Abbrechen', confirmButtonColor: ADMIN_CONFIG.primaryColor, preConfirm: function () { return { title: document.getElementById('swal-title').value.trim(), description: document.getElementById('swal-description').value.trim() }; } }).then(function (result) { if (!result.isConfirmed) return; apiCall({ action: 'update', contribution_id: contributionId, title: result.value.title, description: result.value.description }).then(function (response) { if (response.error) { Swal.fire('Fehler', response.error, 'error'); return; } Swal.fire('Gespeichert!', 'Beitrag wurde aktualisiert.', 'success') .then(function () { location.reload(); }); }); }); } // DELETE: Deletes existing Contributions function deleteContribution(contributionId) { Swal.fire({ title: 'Beitrag löschen?', text: 'Diese Aktion kann nicht rückgängig gemacht werden.', icon: 'warning', showCancelButton: true, confirmButtonText: 'Beitrag löschen', cancelButtonText: 'Abbrechen', confirmButtonColor: '#c62828' }).then(function (result) { if (!result.isConfirmed) return; apiCall({ action: 'delete', contribution_id: contributionId }).then(function (response) { if (response.error) { Swal.fire('Fehler', response.error, 'error'); return; } Swal.fire('Gelöscht!', 'Beitrag wurde gelöscht.', 'success') .then(function () { location.reload(); }); }); }); } // ===================================================================== // Block 9: CRUD Operations for Comments // ===================================================================== // STATUS: Changes Comment Status function changeCommentStatus(commentId, newStatus) { const labels = { approved: 'akzeptieren', rejected: 'ablehnen', pending: 'zurücksetzen' }; Swal.fire({ title: 'Kommentar ' + labels[newStatus] + '?', showCancelButton: true, confirmButtonText: 'Ja', cancelButtonText: 'Abbrechen', confirmButtonColor: ADMIN_CONFIG.primaryColor }).then(function (result) { if (!result.isConfirmed) return; apiCall({ action: 'update_comment', comment_id: commentId, status: newStatus }).then(function (response) { if (response.error) { Swal.fire('Fehler', response.error, 'error'); return; } location.reload(); }); }); } // UPDATE: Edits existing Comments function editModComment(commentId, currentContent) { Swal.fire({ title: 'Kommentar bearbeiten', html: '
' + '' + '' + '
', showCancelButton: true, confirmButtonText: 'Speichern', cancelButtonText: 'Abbrechen', confirmButtonColor: ADMIN_CONFIG.primaryColor, preConfirm: function () { return { content: document.getElementById('swal-comment-content').value.trim() }; } }).then(function (result) { if (!result.isConfirmed) return; apiCall({ action: 'update_comment', comment_id: commentId, content: result.value.content }).then(function (response) { if (response.error) { Swal.fire('Fehler', response.error, 'error'); return; } Swal.fire('Gespeichert!', 'Kommentar wurde aktualisiert.', 'success') .then(function () { location.reload(); }); }); }); } // DELETE: Deletes existing Comments function deleteModComment(commentId) { Swal.fire({ title: 'Kommentar löschen?', text: 'Diese Aktion kann nicht rückgängig gemacht werden.', icon: 'warning', showCancelButton: true, confirmButtonText: 'Löschen', cancelButtonText: 'Abbrechen', confirmButtonColor: '#c62828' }).then(function (result) { if (!result.isConfirmed) return; apiCall({ action: 'delete_comment', comment_id: commentId }).then(function (response) { if (response.error) { Swal.fire('Fehler', response.error, 'error'); return; } Swal.fire('Gelöscht!', 'Kommentar wurde entfernt.', 'success') .then(function () { location.reload(); }); }); }); } // ===================================================================== // Block 10: CRUD Operations for News // ===================================================================== // CREATE: Submits new News Article function createNews() { Swal.fire({ title: 'Neuigkeit hinzufügen', html: '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
', showCancelButton: true, confirmButtonText: 'Veröffentlichen', cancelButtonText: 'Abbrechen', confirmButtonColor: ADMIN_CONFIG.primaryColor, preConfirm: function () { const title = document.getElementById('swal-news-title').value.trim(); const content = document.getElementById('swal-news-content').value.trim(); const author = document.getElementById('swal-news-author').value.trim() || 'Stadtverwaltung'; if (!title || !content) { Swal.showValidationMessage('Titel und Inhalt sind Pflichtfelder.'); return false; } return { title, content, author_name: author }; } }).then(function (result) { if (!result.isConfirmed) return; apiCall({ action: 'create_news', municipality_id: ADMIN_CONFIG.id, title: result.value.title, content: result.value.content, author_name: result.value.author_name }).then(function (response) { if (response.error) { Swal.fire('Fehler', response.error, 'error'); return; } Swal.fire('Veröffentlicht!', 'Neuigkeit wurde veröffentlicht.', 'success') .then(function () { location.reload(); }); }); }); } // UPDATE: Edits existing News function editNews(newsId, currentTitle, currentContent, currentAuthor) { Swal.fire({ title: 'Neuigkeit bearbeiten', html: '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
', showCancelButton: true, confirmButtonText: 'Speichern', cancelButtonText: 'Abbrechen', confirmButtonColor: ADMIN_CONFIG.primaryColor, preConfirm: function () { return { title: document.getElementById('swal-news-title').value.trim(), content: document.getElementById('swal-news-content').value.trim(), author_name: document.getElementById('swal-news-author').value.trim() || 'Stadtverwaltung' }; } }).then(function (result) { if (!result.isConfirmed) return; apiCall({ action: 'update_news', news_id: newsId, title: result.value.title, content: result.value.content, author_name: result.value.author_name }).then(function (response) { if (response.error) { Swal.fire('Fehler', response.error, 'error'); return; } Swal.fire('Gespeichert!', 'Neuigkeit wurde aktualisiert.', 'success') .then(function () { location.reload(); }); }); }); } // DELETE: Deletes existing News function deleteNews(newsId) { Swal.fire({ title: 'Neuigkeit löschen?', text: 'Diese Aktion kann nicht rückgängig gemacht werden.', icon: 'warning', showCancelButton: true, confirmButtonText: 'Löschen', cancelButtonText: 'Abbrechen', confirmButtonColor: '#c62828' }).then(function (result) { if (!result.isConfirmed) return; apiCall({ action: 'delete_news', news_id: newsId }).then(function (response) { if (response.error) { Swal.fire('Fehler', response.error, 'error'); return; } Swal.fire('Gelöscht!', 'Neuigkeit wurde gelöscht.', 'success') .then(function () { location.reload(); }); }); }); }