added map previews in moderation portal
This commit is contained in:
160
public/admin.css
160
public/admin.css
@@ -91,41 +91,6 @@ h2 {
|
||||
|
||||
.section { margin-bottom: 40px; }
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Contribution Rows
|
||||
----------------------------------------------------------------- */
|
||||
.contribution-row {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.contribution-info { flex: 1; }
|
||||
|
||||
.contribution-info .title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.contribution-info .meta {
|
||||
font-size: 0.8rem;
|
||||
color: #5a5a7a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.contribution-info .description {
|
||||
font-size: 0.85rem;
|
||||
color: #5a5a7a;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Badges
|
||||
----------------------------------------------------------------- */
|
||||
@@ -261,11 +226,6 @@ h2 {
|
||||
/* -----------------------------------------------------------------
|
||||
Mobile Responsive
|
||||
----------------------------------------------------------------- */
|
||||
@media (max-width: 768px) {
|
||||
.contribution-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -278,4 +238,124 @@ h2 {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Collapsible Contribution Rows
|
||||
----------------------------------------------------------------- */
|
||||
.contribution-row-collapsible {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contribution-row-header {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.contribution-row-header:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.contribution-row-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contribution-row-summary .title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.meta-inline {
|
||||
font-size: 0.8rem;
|
||||
color: #5a5a7a;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
color: #999;
|
||||
font-size: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Contribution Detail View (expanded)
|
||||
----------------------------------------------------------------- */
|
||||
.contribution-row-detail {
|
||||
padding: 0 16px 16px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.detail-map {
|
||||
width: 200px;
|
||||
height: 160px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
flex-shrink: 0;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-content .description {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: #5a5a7a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-content .meta {
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.btn-map {
|
||||
background: #1565C0;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-map:hover {
|
||||
background: #0d47a1;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Mobile: Detail Layout stacked
|
||||
----------------------------------------------------------------- */
|
||||
@media (max-width: 768px) {
|
||||
.detail-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-map {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.contribution-row-summary {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
175
public/admin.php
175
public/admin.php
@@ -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,19 +170,36 @@ 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']) ?>
|
||||
<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']): ?>
|
||||
<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">
|
||||
@@ -182,12 +212,17 @@ foreach ($stats_rows as $row) {
|
||||
<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']) ?>
|
||||
<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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user