9 Commits

19 changed files with 720 additions and 1168 deletions

View File

@@ -1,48 +0,0 @@
<?php
// ToDo's
// Whitelists oder Prepared Statements gegen SQL-Injection hinzufügen
include 'init.php';
$request = htmlspecialchars($_POST['request'], ENT_QUOTES);
if ($request=='buildings') {
$webgis_id = htmlspecialchars($_POST['webgis_id'], ENT_QUOTES);
try {
$pdo -> query("DELETE FROM buildings WHERE webgis_id = '$webgis_id'");
} catch (PDOException $e) {
echo "ERROR ".$e->getMessage();
}
}
if ($request == 'pipelines') {
$webgis_id = htmlspecialchars($_POST['webgis_id'], ENT_QUOTES);
try {
$pdo -> query("DELETE from pipelines where webgis_id= '$webgis_id' ");
} catch(PDOException $e) {
echo "ERROR ".$e->getMessage();
}
}
if ($request == 'valves') {
$webgis_id = htmlspecialchars($_POST['webgis_id'], ENT_QUOTES);
try {
$pdo -> query("DELETE from valves where webgis_id= '$webgis_id' ");
} catch(PDOException $e) {
echo "ERROR ".$e->getMessage();
}
}
?>

View File

@@ -1,52 +0,0 @@
<?php
// ToDo's
// Whitelists oder Prepared Statements gegen SQL-Injection hinzufügen
// PostgreSQL-Serververbindung
include 'init.php';
// HTTP-POST-Methode für Formulardaten
$table = htmlspecialchars($_POST['table'], ENT_QUOTES);
$field = htmlspecialchars($_POST['field'], ENT_QUOTES);
$value = htmlspecialchars($_POST['value'], ENT_QUOTES);
try {
// Datenbankabfrage
$result = $pdo -> query("SELECT *, ST_AsGeoJSON(geom) as geojson FROM $table WHERE $field = '$value'");
$features = [];
foreach($result as $row) {
// PHP-Objekt erstellen
$geometry = json_decode($row['geojson']);
// PHP-Objekt bereinigen
unset($row['geom']);
unset($row['geojson']);
// JSON-Feature hinzufügen
$feature = [
"type"=>"Feature",
"geometry"=>$geometry,
"properties"=>$row
];
array_push($features, $feature);
};
// Feature-Collection hinzufügen
$featureCollection = [
"type"=>"FeatureCollection",
"features"=>$features
];
echo json_encode($featureCollection);
// Fehlernachricht ausgeben
} catch(PDOException $e) {
echo "ERROR ".$e->getMessage();
}
?>

View File

@@ -1,73 +0,0 @@
<?php
// ToDo's
// Whitelists oder Prepared Statements gegen SQL-Injection hinzufügen
// PostgreSQL-Serververbindung
include 'init.php';
$request = htmlspecialchars($_POST['request'], ENT_QUOTES);
if ($request == 'valves') {
$valve_id = htmlspecialchars($_POST['valve_id'], ENT_QUOTES);
$valve_type = htmlspecialchars($_POST['valve_type'], ENT_QUOTES);
$valve_dma_id = htmlspecialchars($_POST['valve_dma_id'], ENT_QUOTES);
$valve_diameter = htmlspecialchars($_POST['valve_diameter'], ENT_QUOTES);
$valve_visibility = htmlspecialchars($_POST['valve_visibility'], ENT_QUOTES);
$valve_location = htmlspecialchars($_POST['valve_location'], ENT_QUOTES);
$valve_geometry = $_POST['valve_geometry'];
$result = $pdo -> query("SELECT * FROM valves WHERE valve_id = '$valve_id'");
if ($result->rowCount()>0) {
echo "ERROR: Valve ID already exists. Please type in another ID!";
} else {
// Datenbankabfrage
$result = $pdo -> query("INSERT INTO valves(valve_id, valve_type, valve_dma_id, valve_diameter, valve_location, valve_visibility, geom) VALUES ('$valve_id', '$valve_type', '$valve_dma_id', '$valve_diameter', '$valve_location', '$valve_visibility', ST_SetSRID(ST_GeomFromGeoJSON('$valve_geometry'), 4326))");
}
}
if ($request == 'pipelines') {
$pipeline_id = htmlspecialchars($_POST['pipeline_id'], ENT_QUOTES);
$pipeline_category = htmlspecialchars($_POST['pipeline_category'], ENT_QUOTES);
$pipeline_dma_id = htmlspecialchars($_POST['pipeline_dma_id'], ENT_QUOTES);
$pipeline_diameter = htmlspecialchars($_POST['pipeline_diameter'], ENT_QUOTES);
$pipeline_method = htmlspecialchars($_POST['pipeline_method'], ENT_QUOTES);
$pipeline_location = htmlspecialchars($_POST['pipeline_location'], ENT_QUOTES);
$pipeline_geometry = $_POST['pipeline_geometry'];
$result = $pdo -> query("SELECT * FROM pipelines WHERE pipeline_id = '$pipeline_id'");
if ($result->rowCount()>0) {
echo "ERROR: Pipeline ID already exists. Please type in another ID!";
} else {
// Datenbankabfrage
$result = $pdo -> query("INSERT INTO pipelines(pipeline_id, pipeline_category, pipeline_dma_id, pipeline_diameter, pipeline_method, pipeline_location, geom) VALUES ('$pipeline_id', '$pipeline_category', '$pipeline_dma_id', '$pipeline_diameter', '$pipeline_method', '$pipeline_location', ST_SetSRID(ST_GeomFromGeoJSON('$pipeline_geometry'), 4326))");
}
}
if ($request == 'buildings') {
$account_no = htmlspecialchars($_POST['account_no'], ENT_QUOTES);
$building_category = htmlspecialchars($_POST['building_category'], ENT_QUOTES);
$building_dma_id = htmlspecialchars($_POST['building_dma_id'], ENT_QUOTES);
$building_storey = htmlspecialchars($_POST['building_storey'], ENT_QUOTES);
$building_population = htmlspecialchars($_POST['building_population'], ENT_QUOTES);
$building_location = htmlspecialchars($_POST['building_location'], ENT_QUOTES);
$building_geometry = $_POST['building_geometry'];
$result = $pdo -> query("SELECT *from buildings where account_no= '$account_no'");
if ($result->rowCount()>0) {
echo "ERROR: Building ID already exists. Please type in another ID!";
} else {
$sql = $pdo -> query("INSERT INTO buildings(account_no, building_category, building_dma_id, building_storey, building_population, building_location, geom) VALUES ('$account_no', '$building_category', '$building_dma_id', '$building_storey', '$building_population', '$building_location', ST_Force3DZ(ST_SetSRID(ST_GeomFromGeoJSON('$building_geometry'), 4326)))");
}
}
?>

View File

@@ -1,63 +0,0 @@
<?php
// ToDo's
// Whitelists oder Prepared Statements gegen SQL-Injection hinzufügen
// PostgreSQL-Serververbindung
include 'init.php';
// HTTP-POST-Methode für Formulardaten
$table = htmlspecialchars($_POST['table'], ENT_QUOTES);
$dma_id = htmlspecialchars($_POST['dma_id'], ENT_QUOTES);
if($table == 'valves') {
$dma_id_field = "valve_dma_id";
}
if($table == 'buildings') {
$dma_id_field = "building_dma_id";
}
if($table == 'pipelines') {
$dma_id_field = "pipeline_dma_id";
}
try {
// Datenbankabfrage
$result = $pdo -> query("SELECT *, ST_AsGeoJSON(geom) as geojson FROM $table WHERE $dma_id_field = '$dma_id'");
$features = [];
foreach($result as $row) {
// PHP-Objekt erstellen
$geometry = json_decode($row['geojson']);
// PHP-Objekt bereinigen
unset($row['geom']);
unset($row['geojson']);
// JSON-Feature hinzufügen
$feature = [
"type"=>"Feature",
"geometry"=>$geometry,
"properties"=>$row
];
array_push($features, $feature);
};
// Feature-Collection hinzufügen
$featureCollection = [
"type"=>"FeatureCollection",
"features"=>$features
];
echo json_encode($featureCollection);
// Fehlernachricht ausgeben
} catch(PDOException $e) {
echo "ERROR ".$e->getMessage();
}
?>

View File

@@ -1,97 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- jQuery UI -->
<link rel="stylesheet" href="source/jquery-ui.min.css">
<script src="source/jquery-ui.min.js"></script>
<!-- Bootstrap Stylesheet & Skript -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="styles.css">
<!-- Sidebar Plugin -->
<link rel="stylesheet" href="plugins/sidebar/leaflet-sidebar.css">
<script src="plugins/sidebar/leaflet-sidebar.js"></script>
<!-- Button Plugin -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.css">
<script src="https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.js"></script>
<!-- Font Plugin -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<!-- PolylineMeasure Plugin -->
<link rel="stylesheet" href="https://ppete2.github.io/Leaflet.PolylineMeasure/Leaflet.PolylineMeasure.css">
<script src="https://ppete2.github.io/Leaflet.PolylineMeasure/Leaflet.PolylineMeasure.js"></script>
<!-- MousePosition Plugin -->
<link rel="stylesheet" href="plugins/mouseposition/L.Control.MousePosition.css">
<script src="plugins/mouseposition/L.Control.MousePosition.js"></script>
<!-- Geoman Plugin -->
<link rel="stylesheet" href="https://unpkg.com/@geoman-io/leaflet-geoman-free@latest/dist/leaflet-geoman.css">
<script src="https://unpkg.com/@geoman-io/leaflet-geoman-free@latest/dist/leaflet-geoman.js"></script>
<!-- Minimap Plugin -->
<link rel="stylesheet" href="plugins/minimap/Control.MiniMap.min.css">
<script src="plugins/minimap/Control.MiniMap.min.js"></script>
<!-- ajax Plugin -->
<script src="plugins/ajax/leaflet.ajax.js"></script>
</head>
<body>
<div class="popup-container">
<input type="hidden" name="building_database_id" class="updateBuilding" value="something">
<input type="hidden" name="account_no_old" class="updateBuilding" value="something">
<div class="popup-form-group">
<label class="control-label popup-label">Building ID</label>
<input type="text" class="form-control popup-input text-center updateBuilding" value="something" name="account_no">
</div>
<div class="popup-form-group">
<label class="control-label popup-label">Category</label>
<input type="text" class="form-control popup-input text-center updateBuilding" value="something" name="building_category">
</div>
<div class="popup-form-group">
<label class="control-label popup-label">Storey</label>
<input type="number" class="form-control popup-input text-center updateBuilding" value="something" name="building_storey">
</div>
<div class="popup-form-group">
<label class="control-label popup-label">Population</label>
<input type="number" class="form-control popup-input text-center updateBuilding" value="something" name="building_population">
</div>
<div class="popup-form-group">
<label class="control-label popup-label">Location</label>
<input type="text" class="form-control popup-input text-center updateBuilding" value="something" name="building_locationn">
</div>
<div class="popup-button-group">
<button type="submit" class="btn btn-success popup-button">Update</button>
<button type="submit" class="btn btn-danger popup-button">Delete</button>
</div>
</div>
</body>
</html>

View File

@@ -1,98 +0,0 @@
<?php
include 'init.php';
$request = htmlspecialchars($_POST['request'], ENT_QUOTES);
if ($request=='buildings') {
$webgis_id = htmlspecialchars($_POST['webgis_id'], ENT_QUOTES);
$account_no_old = htmlspecialchars($_POST['account_no_old'], ENT_QUOTES);
$account_no = htmlspecialchars($_POST['account_no'], ENT_QUOTES);
$building_category = htmlspecialchars($_POST['building_category'], ENT_QUOTES);
$building_storey = htmlspecialchars($_POST['building_storey'], ENT_QUOTES);
$building_population = htmlspecialchars($_POST['building_population'], ENT_QUOTES);
$building_location = htmlspecialchars($_POST['building_location'], ENT_QUOTES);
$building_dma_id = htmlspecialchars($_POST['building_dma_id'], ENT_QUOTES);
try {
if ($account_no_old != $account_no) {
$result = $pdo -> query("SELECT * FROM buildings WHERE account_no = '$account_no'");
if ($result -> rowCount()>0) {
echo "ERROR: Account Number already exists. Pleas type in another Account Number!";
} else {
$pdo -> query("UPDATE buildings SET account_no = '$account_no', building_category = '$building_category', building_storey = '$building_storey', building_population = '$building_population', building_location = '$building_location', building_dma_id = '$building_dma_id' WHERE webgis_id = '$webgis_id'");
}
} else { $pdo -> query("UPDATE buildings SET account_no = '$account_no', building_category = '$building_category', building_storey = '$building_storey', building_population = '$building_population', building_location = '$building_location', building_dma_id = '$building_dma_id' WHERE webgis_id = '$webgis_id'");
}
} catch (PDOException $e) {
echo "ERROR ".$e->getMessage();
}
}
if ($request == 'pipelines') {
$webgis_id = htmlspecialchars($_POST['webgis_id'], ENT_QUOTES);
$pipeline_id_old = htmlspecialchars($_POST['pipeline_id_old'], ENT_QUOTES);
$pipeline_id = htmlspecialchars($_POST['pipeline_id'], ENT_QUOTES);
$pipeline_dma_id = htmlspecialchars($_POST['pipeline_dma_id'], ENT_QUOTES);
$pipeline_diameter = htmlspecialchars($_POST['pipeline_diameter'], ENT_QUOTES);
$pipeline_location = htmlspecialchars($_POST['pipeline_location'], ENT_QUOTES);
$pipeline_category = htmlspecialchars($_POST['pipeline_category'], ENT_QUOTES);
$pipeline_length = htmlspecialchars($_POST['pipeline_length'], ENT_QUOTES);
try {
if ($pipeline_id_old != $pipeline_id) {
$result = $pdo -> query("SELECT *from pipelines where pipeline_id = '$pipeline_id' ");
if ($result -> rowCount()>0) {
echo "ERROR: Pipeline ID already exists. Please choose a new ID";
} else {
$pdo -> query("UPDATE pipelines set pipeline_id = '$pipeline_id', pipeline_dma_id = '$pipeline_dma_id', pipeline_diameter = '$pipeline_diameter', pipeline_location = '$pipeline_location', pipeline_category='$pipeline_category', pipeline_length='$pipeline_length' where webgis_id = '$webgis_id'");
}
} else {
$pdo -> query("UPDATE pipelines set pipeline_id = '$pipeline_id', pipeline_dma_id = '$pipeline_dma_id', pipeline_diameter = '$pipeline_diameter', pipeline_location = '$pipeline_location', pipeline_category='$pipeline_category', pipeline_length='$pipeline_length' where webgis_id = '$webgis_id'");
}
} catch(PDOException $e) {
echo "ERROR ".$e->getMessage();
}
}
if ($request == 'valves') {
$webgis_id = htmlspecialchars($_POST['webgis_id'], ENT_QUOTES);
$valve_id_old = htmlspecialchars($_POST['valve_id_old'], ENT_QUOTES);
$valve_id = htmlspecialchars($_POST['valve_id'], ENT_QUOTES);
$valve_dma_id = htmlspecialchars($_POST['valve_dma_id'], ENT_QUOTES);
$valve_type = htmlspecialchars($_POST['valve_type'], ENT_QUOTES);
$valve_diameter = htmlspecialchars($_POST['valve_diameter'], ENT_QUOTES);
$valve_location = htmlspecialchars($_POST['valve_location'], ENT_QUOTES);
$valve_visibility = htmlspecialchars($_POST['valve_visibility'], ENT_QUOTES);
try {
if ($valve_id_old != $valve_id) {
$result = $pdo -> query("SELECT *from valves where valve_id = '$valve_id' ");
if ($result -> rowCount()>0) {
echo "ERROR: Valve ID already exists. Please choose a new ID";
} else {
$pdo -> query("UPDATE valves set valve_id = '$valve_id', valve_dma_id = '$valve_dma_id', valve_type = '$valve_type', valve_diameter = '$valve_diameter', valve_location = '$valve_location', valve_visibility = '$valve_visibility' where webgis_id = '$webgis_id' ");
}
} else {
$pdo -> query("UPDATE valves set valve_id = '$valve_id', valve_dma_id = '$valve_dma_id', valve_type = '$valve_type', valve_diameter = '$valve_diameter', valve_location = '$valve_location', valve_visibility = '$valve_visibility' where webgis_id = '$webgis_id' ");
}
} catch(PDOException $e) {
echo "ERROR ".$e->getMessage();
}
}
?>

View File

@@ -131,7 +131,7 @@ $counts['total'] = count($all_contributions);
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moderation — <?= htmlspecialchars($municipality['name']) ?></title> <title>Moderation — <?= htmlspecialchars($municipality['name']) ?></title>
<link rel="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.png') ?>" type="image/png"> <link rel="icon" href="assets/shield-halved-solid-off-black.png" type="image/png">
<!-- Loads CSS Dependencies --> <!-- Loads CSS Dependencies -->
@@ -522,657 +522,28 @@ $counts['total'] = count($all_contributions);
<!-- ============================================================= --> <!-- ============================================================= -->
<!-- JavaScript: Leaflet, Interactions, API Calls --> <!-- Loads JavaScript Dependencies -->
<!-- ============================================================= --> <!-- ============================================================= -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<!-- ============================================================= -->
<!-- Admin Configuration passed to JavaScript -->
<!-- ============================================================= -->
<script> <script>
// Municipality Configuration for Map Previews const ADMIN_CONFIG = {
const MUNICIPALITY_CENTER = [<?= $municipality['center_lat'] ?>, <?= $municipality['center_lng'] ?>]; id: <?= $municipality['municipality_id'] ?>,
const MUNICIPALITY_ID = <?= $municipality['municipality_id'] ?>; name: "<?= htmlspecialchars($municipality['name'], ENT_QUOTES) ?>",
const API_URL = 'api/contributions.php'; slug: "<?= htmlspecialchars($municipality['slug'], ENT_QUOTES) ?>",
const PRIMARY_COLOR = '<?= htmlspecialchars($municipality['primary_color']) ?>'; center: [<?= $municipality['center_lat'] ?>, <?= $municipality['center_lng'] ?>],
zoom: <?= $municipality['default_zoom'] ?>,
// Current Status Filter primaryColor: "<?= htmlspecialchars($municipality['primary_color'], ENT_QUOTES) ?>"
let currentFilter = 'all';
// 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');
}
// =============================================================
// Collapsible Rows
// =============================================================
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);
}
}
}
// =============================================================
// Detail 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 currently active Slide
slides.forEach(function (slide, i) {
if (slide.style.display !== 'none') activeIndex = i;
});
// Calculates next Slide Index (wraps around)
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 and not yet loaded
if (slides[nextIndex].dataset.slide === 'map') {
const mapDiv = slides[nextIndex].querySelector('.detail-map');
if (mapDiv && !mapDiv.dataset.loaded) {
loadMapPreview(mapDiv);
}
}
}
// =============================================================
// Map Preview (Leaflet Mini Map per Contribution)
// =============================================================
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', MUNICIPALITY_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 = '<div style="padding:20px;color:#999;text-align:center;font-size:0.8rem;">Geometrie nicht gefunden.</div>';
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: PRIMARY_COLOR, weight: 3, fillOpacity: 0.2 },
pointToLayer: function (f, latlng) {
return L.circleMarker(latlng, {
radius: 8, color: '#ffffff', weight: 2,
fillColor: PRIMARY_COLOR, 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(MUNICIPALITY_CENTER, 15);
}
mapDiv.dataset.loaded = 'true';
})
.catch(function () {
mapDiv.innerHTML = '<div style="padding:20px;color:#999;text-align:center;font-size:0.8rem;">Karte nicht verfügbar.</div>';
});
}
// =============================================================
// Status Filter
// =============================================================
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/Hides Contribution Rows
let visibleCount = 0;
document.querySelectorAll('.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';
}
// =============================================================
// Sort 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);
} else if (sortBy === 'date-asc') {
return new Date(a.dataset.date) - new Date(b.dataset.date);
} else if (sortBy === 'category') {
return a.dataset.category.localeCompare(b.dataset.category);
}
return 0;
});
// Reappends sorted Rows
rows.forEach(function (row) {
container.appendChild(row);
});
}
// =============================================================
// API Helper
// =============================================================
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(); });
}
// =============================================================
// Change 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: PRIMARY_COLOR
}).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();
});
});
}
// =============================================================
// Edit Contribution
// =============================================================
function editContribution(contributionId, currentTitle, currentDescription) {
Swal.fire({
title: 'Beitrag bearbeiten',
html:
'<div style="text-align:left;">' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Titel</label>' +
'<input id="swal-title" class="swal2-input" style="margin:0;width:100%;" value="' + currentTitle + '">' +
'</div>' +
'<div>' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Beschreibung</label>' +
'<textarea id="swal-description" class="swal2-textarea" style="margin:0;width:100%;">' + currentDescription + '</textarea>' +
'</div>' +
'</div>',
showCancelButton: true,
confirmButtonText: 'Speichern',
cancelButtonText: 'Abbrechen',
confirmButtonColor: PRIMARY_COLOR,
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 Contribution
// =============================================================
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(); });
});
});
}
// =============================================================
// Create News Article
// =============================================================
function createNews() {
Swal.fire({
title: 'Neuigkeit hinzufügen',
html:
'<div style="text-align:left;">' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Titel</label>' +
'<input id="swal-news-title" class="swal2-input" style="margin:0;width:100%;" placeholder="Titel der Neuigkeit">' +
'</div>' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Inhalt</label>' +
'<textarea id="swal-news-content" class="swal2-textarea" style="margin:0;width:100%;" placeholder="Neuigkeit verfassen..."></textarea>' +
'</div>' +
'<div>' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Autor</label>' +
'<input id="swal-news-author" class="swal2-input" style="margin:0;width:100%;" value="Stadtverwaltung">' +
'</div>' +
'</div>',
showCancelButton: true,
confirmButtonText: 'Veröffentlichen',
cancelButtonText: 'Abbrechen',
confirmButtonColor: PRIMARY_COLOR,
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: title, content: content, author_name: author };
}
}).then(function (result) {
if (!result.isConfirmed) return;
const formData = new FormData();
formData.append('action', 'create_news');
formData.append('municipality_id', MUNICIPALITY_ID);
formData.append('title', result.value.title);
formData.append('content', result.value.content);
formData.append('author_name', result.value.author_name);
fetch(API_URL, { method: 'POST', body: formData })
.then(function (r) { return r.json(); })
.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(); });
});
});
}
// =============================================================
// Edit News Article
// =============================================================
function editNews(newsId, currentTitle, currentContent, currentAuthor) {
Swal.fire({
title: 'Neuigkeit bearbeiten',
html:
'<div style="text-align:left;">' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Titel</label>' +
'<input id="swal-news-title" class="swal2-input" style="margin:0;width:100%;" value="' + currentTitle + '">' +
'</div>' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Inhalt</label>' +
'<textarea id="swal-news-content" class="swal2-textarea" style="margin:0;width:100%;">' + currentContent + '</textarea>' +
'</div>' +
'<div>' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Autor</label>' +
'<input id="swal-news-author" class="swal2-input" style="margin:0;width:100%;" value="' + currentAuthor + '">' +
'</div>' +
'</div>',
showCancelButton: true,
confirmButtonText: 'Speichern',
cancelButtonText: 'Abbrechen',
confirmButtonColor: PRIMARY_COLOR,
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;
const formData = new FormData();
formData.append('action', 'update_news');
formData.append('news_id', newsId);
formData.append('title', result.value.title);
formData.append('content', result.value.content);
formData.append('author_name', result.value.author_name);
fetch(API_URL, { method: 'POST', body: formData })
.then(function (r) { return r.json(); })
.then(function (response) {
if (response.error) {
Swal.fire('Fehler', response.error, 'error');
return;
}
Swal.fire('Gespeichert!', 'Neuigkeit wurde aktualisiert.', 'success')
.then(function () { location.reload(); });
});
});
}
// =============================================================
// Create News Article
// =============================================================
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;
const formData = new FormData();
formData.append('action', 'delete_news');
formData.append('news_id', newsId);
fetch(API_URL, { method: 'POST', body: formData })
.then(function (r) { return r.json(); })
.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(); });
});
});
}
// =============================================================
// Sort 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);
} else if (sortBy === 'date-asc') {
return new Date(a.dataset.date) - new Date(b.dataset.date);
} else if (sortBy === 'contribution') {
return a.dataset.contribution.localeCompare(b.dataset.contribution);
}
return 0;
});
rows.forEach(function (row) { container.appendChild(row); });
}
// =============================================================
// Delete 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(); });
});
});
}
// =============================================================
// Filter Comments by Status
// =============================================================
function filterCommentsByStatus(status, tabButton) {
// Updates active Filter Tab (only within Comments Tab)
document.querySelectorAll('#comment-filter-tabs .filter-tab').forEach(function (el) {
el.classList.remove('active');
});
tabButton.classList.add('active');
// Shows/Hides Comment 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';
}
});
document.getElementById('comment-visible-count').textContent = visibleCount + ' Kommentare';
}
// =============================================================
// Change Comment Status (approve, reject, reset)
// =============================================================
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: PRIMARY_COLOR
}).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();
});
});
}
// =============================================================
// Edit Comment Content
// =============================================================
function editModComment(commentId, currentContent) {
Swal.fire({
title: 'Kommentar bearbeiten',
html:
'<div style="text-align:left;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Inhalt</label>' +
'<textarea id="swal-comment-content" class="swal2-textarea" style="margin:0;width:100%;">' + currentContent + '</textarea>' +
'</div>',
showCancelButton: true,
confirmButtonText: 'Speichern',
cancelButtonText: 'Abbrechen',
confirmButtonColor: PRIMARY_COLOR,
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(); });
});
});
}
</script> </script>
<!-- Application Logic -->
<script src="js/admin.js"></script>
</body> </body>
</html> </html>
@@ -1205,7 +576,7 @@ function show_login_page($municipality, $error = null) {
<input type="password" name="password" placeholder="Passwort" autofocus> <input type="password" name="password" placeholder="Passwort" autofocus>
<button type="submit"><i class="fa-solid fa-right-to-bracket"></i> Anmelden</button> <button type="submit"><i class="fa-solid fa-right-to-bracket"></i> Anmelden</button>
</form> </form>
<div class="back-link"><i class="fa fa-arrow-left"></i></i> <a href="index.php">Zurück zum Bürgerportal</a></div> <div class="back-link"><i class="fa fa-arrow-left"></i> <a href="index.php">Zurück zum Bürgerportal</a></div>
</div> </div>
</div> </div>
</body> </body>

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -11,7 +11,7 @@ $municipality = $stmt->fetch();
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impressum — <?= htmlspecialchars($municipality['name']) ?></title> <title>Impressum — <?= htmlspecialchars($municipality['name']) ?></title>
<link rel="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.png') ?>" type="image/png"> <link rel="icon" href="assets/scale-balanced-solid-off-black.png" type="image/png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style> <style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>

View File

@@ -34,7 +34,7 @@ $news_items = $stmt->fetchAll();
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mitmachkarte <?= htmlspecialchars($municipality['name']) ?></title> <title>Mitmachkarte <?= htmlspecialchars($municipality['name']) ?></title>
<link rel="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.png') ?>" type="image/png"> <link rel="icon" href="assets/user-group-solid-off-black.png" type="image/png">
<meta name="description" content="Bürgerbeteiligungsportal. Hinweise und Vorschläge auf der Karte eintragen."> <meta name="description" content="Bürgerbeteiligungsportal. Hinweise und Vorschläge auf der Karte eintragen.">
@@ -89,7 +89,7 @@ $news_items = $stmt->fetchAll();
<header id="app-header"> <header id="app-header">
<div class="header-left"> <div class="header-left">
<?php if (!empty($municipality['logo_path'])): ?> <?php if (!empty($municipality['logo_path'])): ?>
<img src="<?= htmlspecialchars($municipality['logo_path']) ?>" alt="<?= htmlspecialchars($municipality['name']) ?>" class="header-logo" onerror="this.style.display='none'"> <img src="assets/user-group-solid-off-white.png" alt="user-group-solid-off-white" class="header-logo" onerror="this.style.display='none'">
<?php endif; ?> <?php endif; ?>
<h1 class="header-title">Mitmachkarte <?= htmlspecialchars($municipality['name']) ?></h1> <h1 class="header-title">Mitmachkarte <?= htmlspecialchars($municipality['name']) ?></h1>
</div> </div>
@@ -131,9 +131,9 @@ $news_items = $stmt->fetchAll();
<div class="leaflet-sidebar-tabs"> <div class="leaflet-sidebar-tabs">
<ul role="tablist"> <ul role="tablist">
<li><a href="#tab-home" role="tab"><i class="fa-solid fa-house"></i></a></li> <li><a href="#tab-home" role="tab"><i class="fa-solid fa-house"></i></a></li>
<li><a href="#tab-help" role="tab"><i class="fa-solid fa-circle-question"></i></a></li>
<li><a href="#tab-list" role="tab"><i class="fa-solid fa-list"></i></a></li> <li><a href="#tab-list" role="tab"><i class="fa-solid fa-list"></i></a></li>
<li><a href="#tab-news" role="tab"><i class="fa-solid fa-newspaper"></i></a></li> <li><a href="#tab-news" role="tab"><i class="fa-solid fa-newspaper"></i></a></li>
<li><a href="#tab-help" role="tab"><i class="fa-solid fa-circle-question"></i></a></li>
</ul> </ul>
</div> </div>
@@ -148,17 +148,12 @@ $news_items = $stmt->fetchAll();
</h2> </h2>
<div class="sidebar-body"> <div class="sidebar-body">
<p>Willkommen beim Bürgerbeteiligungsportal <strong><?= htmlspecialchars($municipality['name']) ?></strong>.</p> <p>Willkommen beim Bürgerbeteiligungsportal <strong><?= htmlspecialchars($municipality['name']) ?></strong>.</p>
<p>Verwenden Sie die Karte, um Hinweise und Aufgaben für die Stadtverwaltung hinzuzufügen oder bestehende Beiträge der Bürgerschaft zu betrachten.</p> <p>Verwenden Sie die Karte, um Hinweise für die Stadtverwaltung hinzuzufügen oder bestehende Beiträge zu betrachten, zu bewerten und zu kommentieren.</p>
<h3>Kategorien</h3> <h3>Kategorien</h3>
<div id="category-filter"> <div id="category-filter">
<!-- Category Filter Checkboxes — populated by app.js --> <!-- Category Filter Checkboxes — populated by app.js -->
</div> </div>
<h3>Statistik</h3>
<div id="stats-container">
<!-- Contribution Statistics — populated by app.js -->
</div>
</div> </div>
</div> </div>
@@ -178,6 +173,41 @@ $news_items = $stmt->fetchAll();
</div> </div>
</div> </div>
<!-- News Tab -->
<div class="leaflet-sidebar-pane" id="tab-news">
<h2 class="leaflet-sidebar-header">
Neuigkeiten
<span class="leaflet-sidebar-close"><i class="fa-solid fa-xmark"></i></span>
</h2>
<div class="sidebar-body">
<!-- News Search -->
<div class="list-search">
<input type="text" id="news-search-input" placeholder="Neuigkeiten durchsuchen..." class="form-input" oninput="filterNews()">
</div>
<!-- News Items Container -->
<div id="news-list">
<?php if (empty($news_items)): ?>
<p class="empty-state">Noch keine Neuigkeiten veröffentlicht.</p>
<?php else: ?>
<?php foreach ($news_items as $news): ?>
<div class="news-item"
data-title="<?= htmlspecialchars(strtolower($news['title'])) ?>"
data-content="<?= htmlspecialchars(strtolower($news['content'])) ?>"
data-author="<?= htmlspecialchars(strtolower($news['author_name'])) ?>">
<h3><?= htmlspecialchars($news['title']) ?></h3>
<p><?= nl2br(htmlspecialchars($news['content'])) ?></p>
<span class="news-date">
<?= htmlspecialchars($news['author_name']) ?>
· <?= date('d.m.Y', strtotime($news['published_at'])) ?>
</span>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<!-- Help Tab --> <!-- Help Tab -->
<div class="leaflet-sidebar-pane" id="tab-help"> <div class="leaflet-sidebar-pane" id="tab-help">
<h2 class="leaflet-sidebar-header"> <h2 class="leaflet-sidebar-header">
@@ -207,43 +237,6 @@ $news_items = $stmt->fetchAll();
<h3><i class="fa-solid fa-magnifying-glass"></i> Suchen</h3> <h3><i class="fa-solid fa-magnifying-glass"></i> Suchen</h3>
<p>Verwenden Sie die Adresssuche rechts, um schnell den richtigen Ort auf der Mitmachkarte zu finden.</p> <p>Verwenden Sie die Adresssuche rechts, um schnell den richtigen Ort auf der Mitmachkarte zu finden.</p>
</div>
</div>
<!-- News Tab -->
<div class="leaflet-sidebar-pane" id="tab-news">
<h2 class="leaflet-sidebar-header">
Neuigkeiten
<span class="leaflet-sidebar-close"><i class="fa-solid fa-xmark"></i></span>
</h2>
<div class="sidebar-body">
<!-- News Search -->
<div class="list-search">
<input type="text" id="news-search-input" placeholder="Neuigkeiten durchsuchen..." class="form-input" oninput="filterNews()">
</div>
<!-- News Items Container -->
<div id="news-list">
<?php if (empty($news_items)): ?>
<p style="text-align:center;color:#999;padding:20px;">Noch keine Neuigkeiten veröffentlicht.</p>
<?php else: ?>
<?php foreach ($news_items as $news): ?>
<div class="news-item"
data-title="<?= htmlspecialchars(strtolower($news['title'])) ?>"
data-content="<?= htmlspecialchars(strtolower($news['content'])) ?>"
data-author="<?= htmlspecialchars(strtolower($news['author_name'])) ?>">
<h3><?= htmlspecialchars($news['title']) ?></h3>
<p><?= nl2br(htmlspecialchars($news['content'])) ?></p>
<span class="news-date">
<?= htmlspecialchars($news['author_name']) ?>
· <?= date('d.m.Y', strtotime($news['published_at'])) ?>
</span>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,637 @@
// =====================================================================
// 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 = '<div style="padding:20px;color:#999;text-align:center;font-size:0.8rem;">Geometrie nicht gefunden.</div>';
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 = '<div style="padding:20px;color:#999;text-align:center;font-size:0.8rem;">Karte nicht verfügbar.</div>';
});
}
// =====================================================================
// 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:
'<div style="text-align:left;">' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Titel</label>' +
'<input id="swal-title" class="swal2-input" style="margin:0;width:100%;" value="' + escapeHtml(currentTitle) + '">' +
'</div>' +
'<div>' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Beschreibung</label>' +
'<textarea id="swal-description" class="swal2-textarea" style="margin:0;width:100%;">' + escapeHtml(currentDescription) + '</textarea>' +
'</div>' +
'</div>',
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:
'<div style="text-align:left;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Inhalt</label>' +
'<textarea id="swal-comment-content" class="swal2-textarea" style="margin:0;width:100%;">' + escapeHtml(currentContent) + '</textarea>' +
'</div>',
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:
'<div style="text-align:left;">' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Titel</label>' +
'<input id="swal-news-title" class="swal2-input" style="margin:0;width:100%;" placeholder="Titel der Neuigkeit">' +
'</div>' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Inhalt</label>' +
'<textarea id="swal-news-content" class="swal2-textarea" style="margin:0;width:100%;" placeholder="Neuigkeit verfassen..."></textarea>' +
'</div>' +
'<div>' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Autor</label>' +
'<input id="swal-news-author" class="swal2-input" style="margin:0;width:100%;" value="Stadtverwaltung">' +
'</div>' +
'</div>',
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:
'<div style="text-align:left;">' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Titel</label>' +
'<input id="swal-news-title" class="swal2-input" style="margin:0;width:100%;" value="' + escapeHtml(currentTitle) + '">' +
'</div>' +
'<div style="margin-bottom:12px;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Inhalt</label>' +
'<textarea id="swal-news-content" class="swal2-textarea" style="margin:0;width:100%;">' + escapeHtml(currentContent) + '</textarea>' +
'</div>' +
'<div>' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Autor</label>' +
'<input id="swal-news-author" class="swal2-input" style="margin:0;width:100%;" value="' + escapeHtml(currentAuthor) + '">' +
'</div>' +
'</div>',
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(); });
});
});
}

View File

@@ -346,7 +346,7 @@ function loadContributions() {
layerControl.addOverlay(contributionsLayer, '<i class="fa-solid fa-map-pin" style="color:#C00000;"></i> Beiträge'); layerControl.addOverlay(contributionsLayer, '<i class="fa-solid fa-map-pin" style="color:#C00000;"></i> Beiträge');
// Update Sidebar List and Statistics // Update Sidebar List and Statistics
updateContributionsList(); updateContributionsList();
updateStatistics(); buildCategoryFilter();
}); });
} }
@@ -766,7 +766,7 @@ function updateContributionsList() {
// Builds HTML // Builds HTML
if (filtered.length === 0) { if (filtered.length === 0) {
container.innerHTML = '<p style="text-align:center;color:#999;padding:20px;">Keine Beiträge gefunden.</p>'; container.innerHTML = '<p class="empty-state">Keine Beiträge gefunden.</p>';
return; return;
} }
@@ -830,22 +830,33 @@ document.getElementById('list-search-input').addEventListener('input', function
// Block 12: Sidebar Category Filter and Statistics // Block 12: Sidebar Category Filter and Statistics
// ===================================================================== // =====================================================================
// Builds Category Filter Checkboxes // Builds Category Filter Checkboxes with Counts
function buildCategoryFilter() { function buildCategoryFilter() {
const container = document.getElementById('category-filter'); const container = document.getElementById('category-filter');
const counts = {};
contributionsData.forEach(function (f) {
const cat = f.properties.category;
counts[cat] = (counts[cat] || 0) + 1;
});
const total = contributionsData.length;
let html = ''; let html = '';
for (const key in CATEGORIES) { for (const key in CATEGORIES) {
const cat = CATEGORIES[key]; const cat = CATEGORIES[key];
const checked = activeFilters.indexOf(key) !== -1 ? 'checked' : ''; const checked = activeFilters.indexOf(key) !== -1 ? 'checked' : '';
const count = counts[key] || 0;
html += '' + html += '<label style="display:flex;align-items:center;gap:8px;margin-bottom:6px;cursor:pointer;font-size:0.85rem;color:var(--color-text-secondary)">' +
'<label style="display:flex;align-items:center;gap:8px;margin-bottom:6px;cursor:pointer;">' +
'<input type="checkbox" value="' + key + '" ' + checked + ' onchange="toggleCategoryFilter(this)">' + '<input type="checkbox" value="' + key + '" ' + checked + ' onchange="toggleCategoryFilter(this)">' +
'<span>' + categoryIcon(cat) + ' ' + cat.label + '</span>' + categoryIcon(cat) +
'<span>' + cat.label + ' (' + count + ')</span>' +
'</label>'; '</label>';
} }
html += '<p style="margin-top:10px;font-size:0.85rem;color:var(--color-text-secondary)"><strong>' + total + '</strong> Beiträge insgesamt</p>';
container.innerHTML = html; container.innerHTML = html;
} }
@@ -892,34 +903,6 @@ function toggleCategoryFilter(checkbox) {
updateContributionsList(); updateContributionsList();
} }
// Updates Statistics in Home Tab
function updateStatistics() {
const container = document.getElementById('stats-container');
const total = contributionsData.length;
// Counts per Category
const counts = {};
contributionsData.forEach(function (f) {
const cat = f.properties.category;
counts[cat] = (counts[cat] || 0) + 1;
});
let html = '<p style="font-size:0.9rem;"><strong>' + total + '</strong> Beiträge insgesamt</p>';
for (const key in CATEGORIES) {
const cat = CATEGORIES[key];
const count = counts[key] || 0;
if (count > 0) {
html += '<div style="display:flex;align-items:center;gap:8px;margin:4px 0;font-size:0.85rem;">' +
categoryIcon(cat) + ' ' +
cat.label + ': ' + count +
'</div>';
}
}
container.innerHTML = html;
}
// ===================================================================== // =====================================================================
// Block 13: Modals — Welcome, Login, Info, Privacy, Imprint // Block 13: Modals — Welcome, Login, Info, Privacy, Imprint
@@ -1034,7 +1017,7 @@ function escapeHtml(text) {
// Returns a colored Font Awesome Icon HTML String for a Category // Returns a colored Font Awesome Icon HTML String for a Category
function categoryIcon(cat) { function categoryIcon(cat) {
return '<i class="fa-solid ' + cat.faIcon + '" style="color:' + cat.color + ';"></i>'; return '<i class="fa-solid ' + cat.faIcon + ' fa-fw" style="color:' + cat.color + ';"></i>';
} }
// Reverse Geocodes Coordinates and saves Address to Contribution via API // Reverse Geocodes Coordinates and saves Address to Contribution via API

View File

@@ -11,7 +11,7 @@ $municipality = $stmt->fetch();
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutz — <?= htmlspecialchars($municipality['name']) ?></title> <title>Datenschutz — <?= htmlspecialchars($municipality['name']) ?></title>
<link rel="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.png') ?>" type="image/png"> <link rel="icon" href="assets/lock-solid-off-black.png" type="image/png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style> <style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>

View File

@@ -464,6 +464,7 @@ select.form-input { cursor: pointer; }
margin-bottom: var(--space-sm); margin-bottom: var(--space-sm);
line-height: 1.5; line-height: 1.5;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 0.85rem;
} }

View File

@@ -5,10 +5,8 @@ Citizen Participation Portal for Lohne (Oldenburg).
## Project Structure ## Project Structure
- `migrations/` — versioned SQL Schema Migrations - `migrations/` — versioned SQL Schema Migrations
- `api/` — Backend (PHP)
- `public/` — Frontend (HTML, CSS, JS) - `public/` — Frontend (HTML, CSS, JS)
- `scripts/` — Maintenance Scripts (backup, deployment) - `scripts/` — Maintenance Scripts (backup, deployment)
- `legacy/` — Reference Code from Prototype
## Local Setup ## Local Setup