added map previews in moderation portal

This commit is contained in:
2026-04-20 17:04:33 +02:00
parent ad475390ce
commit 79b51e039f
2 changed files with 301 additions and 70 deletions

View File

@@ -102,6 +102,17 @@ foreach ($stats_rows as $row) {
$stats[$row['status']] = $row['count'];
}
// Category Labels for German Display
$category_labels = [
'mobility' => '🚲 Mobilität',
'building' => '🏗️ Bauen',
'energy' => '⚡ Energie',
'environment' => '🌳 Umwelt',
'industry' => '🏭 Industrie',
'consumption' => '🛒 Konsum',
'other' => '📌 Sonstiges'
];
// -----------------------------------------------------------------
// Render Main Page
// -----------------------------------------------------------------
@@ -128,6 +139,7 @@ foreach ($stats_rows as $row) {
<div class="admin-container">
<!-- Statistics -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number"><?= ($stats['pending'] ?? 0) ?></div>
@@ -147,6 +159,7 @@ foreach ($stats_rows as $row) {
</div>
</div>
<!-- Pending Contributions -->
<div class="section">
<h2><i class="fa-solid fa-clock"></i> Ausstehende Beiträge (<?= count($pending) ?>)</h2>
@@ -157,37 +170,59 @@ foreach ($stats_rows as $row) {
</div>
<?php else: ?>
<?php foreach ($pending as $item): ?>
<div class="contribution-row">
<div class="contribution-info">
<div class="title"><?= htmlspecialchars($item['title']) ?></div>
<div class="meta">
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
<div class="contribution-row-collapsible">
<div class="contribution-row-header" onclick="toggleContribution(this)">
<div class="contribution-row-summary">
<span class="title"><?= htmlspecialchars($item['title']) ?></span>
<span class="badge badge-pending">ausstehend</span>
· <?= htmlspecialchars($item['category']) ?>
· <?= htmlspecialchars($item['author_name']) ?>
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
<span class="meta-inline">
<?= $category_labels[$item['category']] ?? $item['category'] ?>
· <?= htmlspecialchars($item['author_name']) ?>
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
</span>
</div>
<?php if ($item['description']): ?>
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
<?php endif; ?>
<i class="fa-solid fa-chevron-down collapse-icon"></i>
</div>
<div class="action-buttons">
<form method="POST">
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
<input type="hidden" name="mod_action" value="approved">
<button type="submit" class="btn btn-approve"><i class="fa-solid fa-check"></i> Freigeben</button>
</form>
<form method="POST">
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
<input type="hidden" name="mod_action" value="rejected">
<button type="submit" class="btn btn-reject"><i class="fa-solid fa-xmark"></i> Ablehnen</button>
</form>
<div class="contribution-row-detail" style="display:none;">
<div class="detail-layout">
<div class="detail-map" id="map-<?= $item['contribution_id'] ?>"
data-contribution-id="<?= $item['contribution_id'] ?>">
</div>
<div class="detail-content">
<?php if ($item['description']): ?>
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
<?php else: ?>
<div class="description" style="color:#bbb;font-style:italic;">Keine Beschreibung.</div>
<?php endif; ?>
<div class="meta">
<i class="fa-solid fa-user"></i> <?= htmlspecialchars($item['author_name']) ?>
· <i class="fa-solid fa-calendar"></i> <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
</div>
</div>
</div>
<div class="action-buttons">
<form method="POST">
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
<input type="hidden" name="mod_action" value="approved">
<button type="submit" class="btn btn-approve"><i class="fa-solid fa-check"></i> Freigeben</button>
</form>
<form method="POST">
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
<input type="hidden" name="mod_action" value="rejected">
<button type="submit" class="btn btn-reject"><i class="fa-solid fa-xmark"></i> Ablehnen</button>
</form>
<a href="index.php#contribution-<?= $item['contribution_id'] ?>" class="btn btn-map" target="_blank">
<i class="fa-solid fa-map-location-dot"></i> Auf Karte
</a>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Recently Moderated -->
<div class="section">
<h2><i class="fa-solid fa-history"></i> Kürzlich moderiert</h2>
@@ -195,15 +230,36 @@ foreach ($stats_rows as $row) {
<div class="empty-state">Noch keine moderierten Beiträge.</div>
<?php else: ?>
<?php foreach ($moderated as $item): ?>
<div class="contribution-row">
<div class="contribution-info">
<div class="title"><?= htmlspecialchars($item['title']) ?></div>
<div class="meta">
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
<div class="contribution-row-collapsible">
<div class="contribution-row-header" onclick="toggleContribution(this)">
<div class="contribution-row-summary">
<span class="title"><?= htmlspecialchars($item['title']) ?></span>
<span class="badge badge-<?= $item['status'] ?>"><?= $item['status'] === 'approved' ? 'freigegeben' : 'abgelehnt' ?></span>
· <?= htmlspecialchars($item['category']) ?>
· <?= htmlspecialchars($item['author_name']) ?>
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
<span class="meta-inline">
<?= $category_labels[$item['category']] ?? $item['category'] ?>
· <?= htmlspecialchars($item['author_name']) ?>
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
</span>
</div>
<i class="fa-solid fa-chevron-down collapse-icon"></i>
</div>
<div class="contribution-row-detail" style="display:none;">
<div class="detail-layout">
<div class="detail-map" id="map-<?= $item['contribution_id'] ?>"
data-contribution-id="<?= $item['contribution_id'] ?>">
</div>
<div class="detail-content">
<?php if ($item['description'] ?? false): ?>
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
<?php else: ?>
<div class="description" style="color:#bbb;font-style:italic;">Keine Beschreibung.</div>
<?php endif; ?>
<div class="meta">
<i class="fa-solid fa-user"></i> <?= htmlspecialchars($item['author_name']) ?>
· <i class="fa-solid fa-calendar"></i> <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
</div>
</div>
</div>
</div>
</div>
@@ -213,6 +269,101 @@ foreach ($stats_rows as $row) {
</div>
<!-- Leaflet for Map Previews -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<script>
// Municipality Configuration for Map Previews
var MUNICIPALITY_CENTER = [<?= $municipality['center_lat'] ?>, <?= $municipality['center_lng'] ?>];
var API_URL = 'api/contributions.php';
// Toggle Contribution Detail View
function toggleContribution(header) {
var detail = header.nextElementSibling;
var icon = header.querySelector('.collapse-icon');
var isOpen = detail.style.display !== 'none';
if (isOpen) {
detail.style.display = 'none';
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
} else {
detail.style.display = 'block';
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
// Load Map Preview if not already loaded
var mapDiv = detail.querySelector('.detail-map');
if (mapDiv && !mapDiv.dataset.loaded) {
loadMapPreview(mapDiv);
}
}
}
// Load a small Leaflet Map Preview for a Contribution
function loadMapPreview(mapDiv) {
var contributionId = mapDiv.dataset.contributionId;
// Fetch Contribution Geometry from API
var formData = new FormData();
formData.append('action', 'read');
formData.append('municipality_id', '<?= $municipality['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;
// Find the specific Contribution
var 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;">Keine Geometrie gefunden.</div>';
return;
}
// Create small Leaflet Map
var 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);
// Add Geometry to Map
var geojsonLayer = L.geoJSON(feature, {
style: { color: '#c62828', weight: 3, fillOpacity: 0.3 },
pointToLayer: function (f, latlng) {
return L.circleMarker(latlng, {
radius: 8, color: '#c62828', fillColor: '#ef5350', fillOpacity: 0.8, weight: 2
});
}
}).addTo(miniMap);
// Fit Map to Geometry Bounds
var bounds = geojsonLayer.getBounds();
if (bounds.isValid()) {
miniMap.fitBounds(bounds, { padding: [20, 20], 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;">Karte nicht verfügbar.</div>';
});
}
</script>
</body>
</html>