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; }
|
.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
|
Badges
|
||||||
----------------------------------------------------------------- */
|
----------------------------------------------------------------- */
|
||||||
@@ -261,11 +226,6 @@ h2 {
|
|||||||
/* -----------------------------------------------------------------
|
/* -----------------------------------------------------------------
|
||||||
Mobile Responsive
|
Mobile Responsive
|
||||||
----------------------------------------------------------------- */
|
----------------------------------------------------------------- */
|
||||||
@media (max-width: 768px) {
|
|
||||||
.contribution-row {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -278,4 +238,124 @@ h2 {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
211
public/admin.php
211
public/admin.php
@@ -102,6 +102,17 @@ foreach ($stats_rows as $row) {
|
|||||||
$stats[$row['status']] = $row['count'];
|
$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
|
// Render Main Page
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
@@ -128,6 +139,7 @@ foreach ($stats_rows as $row) {
|
|||||||
|
|
||||||
<div class="admin-container">
|
<div class="admin-container">
|
||||||
|
|
||||||
|
<!-- Statistics -->
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-number"><?= ($stats['pending'] ?? 0) ?></div>
|
<div class="stat-number"><?= ($stats['pending'] ?? 0) ?></div>
|
||||||
@@ -147,6 +159,7 @@ foreach ($stats_rows as $row) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pending Contributions -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2><i class="fa-solid fa-clock"></i> Ausstehende Beiträge (<?= count($pending) ?>)</h2>
|
<h2><i class="fa-solid fa-clock"></i> Ausstehende Beiträge (<?= count($pending) ?>)</h2>
|
||||||
|
|
||||||
@@ -157,37 +170,59 @@ foreach ($stats_rows as $row) {
|
|||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($pending as $item): ?>
|
<?php foreach ($pending as $item): ?>
|
||||||
<div class="contribution-row">
|
<div class="contribution-row-collapsible">
|
||||||
<div class="contribution-info">
|
<div class="contribution-row-header" onclick="toggleContribution(this)">
|
||||||
<div class="title"><?= htmlspecialchars($item['title']) ?></div>
|
<div class="contribution-row-summary">
|
||||||
<div class="meta">
|
<span class="title"><?= htmlspecialchars($item['title']) ?></span>
|
||||||
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
|
|
||||||
<span class="badge badge-pending">ausstehend</span>
|
<span class="badge badge-pending">ausstehend</span>
|
||||||
· <?= htmlspecialchars($item['category']) ?>
|
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
|
||||||
· <?= htmlspecialchars($item['author_name']) ?>
|
<span class="meta-inline">
|
||||||
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
|
<?= $category_labels[$item['category']] ?? $item['category'] ?>
|
||||||
|
· <?= htmlspecialchars($item['author_name']) ?>
|
||||||
|
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($item['description']): ?>
|
<i class="fa-solid fa-chevron-down collapse-icon"></i>
|
||||||
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="contribution-row-detail" style="display:none;">
|
||||||
<form method="POST">
|
<div class="detail-layout">
|
||||||
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
|
<div class="detail-map" id="map-<?= $item['contribution_id'] ?>"
|
||||||
<input type="hidden" name="mod_action" value="approved">
|
data-contribution-id="<?= $item['contribution_id'] ?>">
|
||||||
<button type="submit" class="btn btn-approve"><i class="fa-solid fa-check"></i> Freigeben</button>
|
</div>
|
||||||
</form>
|
<div class="detail-content">
|
||||||
<form method="POST">
|
<?php if ($item['description']): ?>
|
||||||
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
|
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
|
||||||
<input type="hidden" name="mod_action" value="rejected">
|
<?php else: ?>
|
||||||
<button type="submit" class="btn btn-reject"><i class="fa-solid fa-xmark"></i> Ablehnen</button>
|
<div class="description" style="color:#bbb;font-style:italic;">Keine Beschreibung.</div>
|
||||||
</form>
|
<?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>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Recently Moderated -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2><i class="fa-solid fa-history"></i> Kürzlich moderiert</h2>
|
<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>
|
<div class="empty-state">Noch keine moderierten Beiträge.</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($moderated as $item): ?>
|
<?php foreach ($moderated as $item): ?>
|
||||||
<div class="contribution-row">
|
<div class="contribution-row-collapsible">
|
||||||
<div class="contribution-info">
|
<div class="contribution-row-header" onclick="toggleContribution(this)">
|
||||||
<div class="title"><?= htmlspecialchars($item['title']) ?></div>
|
<div class="contribution-row-summary">
|
||||||
<div class="meta">
|
<span class="title"><?= htmlspecialchars($item['title']) ?></span>
|
||||||
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
|
|
||||||
<span class="badge badge-<?= $item['status'] ?>"><?= $item['status'] === 'approved' ? 'freigegeben' : 'abgelehnt' ?></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>
|
||||||
· <?= htmlspecialchars($item['author_name']) ?>
|
<span class="meta-inline">
|
||||||
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
|
<?= $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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,6 +269,101 @@ foreach ($stats_rows as $row) {
|
|||||||
|
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user