From f30a01615e5bc1eb26890923bd699e8af086c8b4 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Wed, 22 Apr 2026 14:16:13 +0200 Subject: [PATCH 01/11] bugfix like dislikes disappeared when reopening closed contribution popup --- public/js/app.js | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index a916029..67e5947 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -367,7 +367,7 @@ function styleLinePolygon(feature) { // Block 9: Feature Popups for Read, Edit, Delete and Vote // ===================================================================== -function bindFeaturePopup(feature, layer) { +function buildPopupHtml(feature) { const props = feature.properties; const cat = CATEGORIES[props.category] || CATEGORIES.other; @@ -377,8 +377,7 @@ function bindFeaturePopup(feature, layer) { day: '2-digit', month: '2-digit', year: 'numeric' }); - // Builds Popup on Click - const html = '' + + return '' + '' : '') + ''; +} - layer.bindPopup(html, { maxWidth: 320, minWidth: 240 }); +// Binds Popup and Tooltip to Feature Layer +function bindFeaturePopup(feature, layer) { + const cat = CATEGORIES[feature.properties.category] || CATEGORIES.other; - // Builds Tooltip on Hover - layer.bindTooltip(categoryIcon(cat) + ' ' + escapeHtml(props.title), { + // Rebuilts if Popup opens + layer.bindPopup(function () { return buildPopupHtml(feature); }, { maxWidth: 320, minWidth: 240 }); + + // Tooltip on Hover + layer.bindTooltip(categoryIcon(cat) + ' ' + escapeHtml(feature.properties.title), { direction: 'top', offset: [0, -10] }); @@ -575,39 +580,53 @@ function voteContribution(contributionId, voteType) { const likesSpan = document.getElementById('likes-' + contributionId); const dislikesSpan = document.getElementById('dislikes-' + contributionId); + // Finds Feature in Contributions to update Properties + const feature = contributionsData.find(function (f) { + return f.properties.contribution_id === contributionId; + }); + if (response.action === 'created') { - // New Vote — Highlights Button and updates Count userVotes[contributionId] = voteType; if (voteType === 'like') { likeBtn.classList.add('liked'); likesSpan.textContent = parseInt(likesSpan.textContent) + 1; + if (feature) feature.properties.likes_count++; } else { dislikeBtn.classList.add('disliked'); dislikesSpan.textContent = parseInt(dislikesSpan.textContent) + 1; + if (feature) feature.properties.dislikes_count++; } } else if (response.action === 'removed') { - // Vote removed — Removes Button Highlight and updates Count delete userVotes[contributionId]; if (voteType === 'like') { likeBtn.classList.remove('liked'); likesSpan.textContent = Math.max(0, parseInt(likesSpan.textContent) - 1); + if (feature) feature.properties.likes_count = Math.max(0, feature.properties.likes_count - 1); } else { dislikeBtn.classList.remove('disliked'); dislikesSpan.textContent = Math.max(0, parseInt(dislikesSpan.textContent) - 1); + if (feature) feature.properties.dislikes_count = Math.max(0, feature.properties.dislikes_count - 1); } } else if (response.action === 'changed') { - // Vote changed — Switches Highlights and updates both Counts userVotes[contributionId] = voteType; if (voteType === 'like') { likeBtn.classList.add('liked'); dislikeBtn.classList.remove('disliked'); likesSpan.textContent = parseInt(likesSpan.textContent) + 1; dislikesSpan.textContent = Math.max(0, parseInt(dislikesSpan.textContent) - 1); + if (feature) { + feature.properties.likes_count++; + feature.properties.dislikes_count = Math.max(0, feature.properties.dislikes_count - 1); + } } else { dislikeBtn.classList.add('disliked'); likeBtn.classList.remove('liked'); dislikesSpan.textContent = parseInt(dislikesSpan.textContent) + 1; likesSpan.textContent = Math.max(0, parseInt(likesSpan.textContent) - 1); + if (feature) { + feature.properties.dislikes_count++; + feature.properties.likes_count = Math.max(0, feature.properties.likes_count - 1); + } } } }); -- 2.49.1 From 9d7eb25d1f67619e6d9843e6881af5fabc4a1fa3 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Wed, 22 Apr 2026 14:32:13 +0200 Subject: [PATCH 02/11] get categories function for category definition in moderation page --- public/api/db.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/public/api/db.php b/public/api/db.php index e3b08d9..80d900c 100644 --- a/public/api/db.php +++ b/public/api/db.php @@ -91,4 +91,23 @@ function get_db() { } return $pdo; +} + +// --------------------------------------------------------------------- +// Category Definitions +// Returns associative Array of Category Keys to Labels, Icons, +// and Colors. Shared between Citizen Participation Portal and +// Moderation Page. +// ToDo: Move to Database Table. +// --------------------------------------------------------------------- +function get_categories() { + return [ + 'consumption' => ['label' => 'Geschäfte', 'faIcon' => 'fa-cart-shopping', 'color' => '#C00000'], + 'building' => ['label' => 'Bauen', 'faIcon' => 'fa-building', 'color' => '#E65100'], + 'energy' => ['label' => 'Energie', 'faIcon' => 'fa-bolt', 'color' => '#FFC000'], + 'environment' => ['label' => 'Umwelt', 'faIcon' => 'fa-seedling', 'color' => '#92D050'], + 'mobility' => ['label' => 'Mobilität', 'faIcon' => 'fa-bus', 'color' => '#0070C0'], + 'industry' => ['label' => 'Industrie', 'faIcon' => 'fa-industry', 'color' => '#7030A0'], + 'other' => ['label' => 'Sonstiges', 'faIcon' => 'fa-thumbtack', 'color' => '#7F7F7F'], + ]; } \ No newline at end of file -- 2.49.1 From 27d41c08477a10f8f2d4c77a56680e5c7faf7767 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Wed, 22 Apr 2026 14:34:03 +0200 Subject: [PATCH 03/11] simplified admin and mod authentification for new moderation page --- public/api/auth.php | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/public/api/auth.php b/public/api/auth.php index d38695f..3a0f71c 100644 --- a/public/api/auth.php +++ b/public/api/auth.php @@ -2,15 +2,10 @@ // ===================================================================== // Admin Authentication Helper // Provides simple Password-based Session Authentication for the -// Moderation Page. Uses ADMIN_PASSWORD from .env File. +// Moderation Page. Reads Password from .env File. // ToDo: Replace with full User Authentication in Phase 3-3. // ===================================================================== -// Reads Admin Password from Environment -function get_admin_password() { - return getenv('ADMIN_PASSWORD'); -} - // Checks if current Session is authenticated as Admin function is_admin() { return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true; @@ -18,7 +13,7 @@ function is_admin() { // Authenticates with Password, returns true on Success function admin_login($password) { - $correct = get_admin_password(); + $correct = getenv('ADMIN_PASSWORD'); if ($correct && $password === $correct) { $_SESSION['is_admin'] = true; return true; @@ -30,12 +25,4 @@ function admin_login($password) { function admin_logout() { $_SESSION['is_admin'] = false; session_destroy(); -} - -// Redirects to Login if not authenticated -function require_admin() { - if (!is_admin()) { - header('Location: admin.php?page=login'); - exit; - } } \ No newline at end of file -- 2.49.1 From adf863934ef6fe00f6db26feb98854ea34d61b0f Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Wed, 22 Apr 2026 14:39:38 +0200 Subject: [PATCH 04/11] rebuild moderation page with filter and sorting functions, CRUD operations, map preview function and shared categories --- public/admin.css | 512 +++++++++++++++++++++++------------ public/admin.php | 682 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 807 insertions(+), 387 deletions(-) diff --git a/public/admin.css b/public/admin.css index 788a14a..adfb977 100644 --- a/public/admin.css +++ b/public/admin.css @@ -1,7 +1,12 @@ /* ===================================================================== - Moderation Page Styles + Moderation Page — Styles + Separate Stylesheet for the Admin Moderation Interface. ===================================================================== */ + +/* ----------------------------------------------------------------- + Base + ----------------------------------------------------------------- */ * { box-sizing: border-box; margin: 0; padding: 0; } body { @@ -11,44 +16,52 @@ body { font-size: 15px; } + /* ----------------------------------------------------------------- Header ----------------------------------------------------------------- */ .admin-header { background: var(--color-primary); color: white; - padding: 16px 24px; + padding: 14px 24px; display: flex; justify-content: space-between; align-items: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } -.admin-header h1 { font-size: 1.2rem; } - -.admin-header a { - color: white; - text-decoration: none; - opacity: 0.8; - font-size: 0.85rem; +.admin-header h1 { + font-size: 1.15rem; + font-weight: 600; } -.admin-header a:hover { opacity: 1; } - .admin-nav { display: flex; gap: 16px; align-items: center; } +.admin-nav a { + color: white; + text-decoration: none; + opacity: 0.8; + font-size: 0.85rem; + transition: opacity 150ms ease; +} + +.admin-nav a:hover { opacity: 1; } + + /* ----------------------------------------------------------------- Container ----------------------------------------------------------------- */ .admin-container { - max-width: 900px; + max-width: 960px; margin: 24px auto; padding: 0 16px; } + /* ----------------------------------------------------------------- Statistics Cards ----------------------------------------------------------------- */ @@ -56,7 +69,7 @@ body { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; - margin-bottom: 32px; + margin-bottom: 28px; } .stat-card { @@ -79,17 +92,230 @@ body { margin-top: 4px; } + /* ----------------------------------------------------------------- - Section Headers + Filter Tabs ----------------------------------------------------------------- */ -h2 { - font-size: 1.1rem; - margin-bottom: 16px; - padding-bottom: 8px; - border-bottom: 2px solid var(--color-primary); +.filter-tabs { + display: flex; + gap: 4px; + margin-bottom: 20px; + border-bottom: 2px solid #e0e0e0; + padding-bottom: 0; } -.section { margin-bottom: 40px; } +.filter-tab { + padding: 8px 16px; + border: none; + background: none; + font-family: inherit; + font-size: 0.85rem; + font-weight: 600; + color: #5a5a7a; + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + transition: color 150ms ease, border-color 150ms ease; +} + +.filter-tab:hover { + color: var(--color-primary); +} + +.filter-tab.active { + color: var(--color-primary); + border-bottom-color: var(--color-primary); +} + +.filter-tab .tab-count { + background: #e0e0e0; + color: #5a5a7a; + font-size: 0.7rem; + padding: 1px 6px; + border-radius: 10px; + margin-left: 4px; +} + +.filter-tab.active .tab-count { + background: var(--color-primary); + color: white; +} + + +/* ----------------------------------------------------------------- + Sort Controls + ----------------------------------------------------------------- */ +.sort-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + font-size: 0.85rem; + color: #5a5a7a; +} + +.sort-controls select { + padding: 4px 8px; + border: 1px solid #e0e0e0; + border-radius: 4px; + font-family: inherit; + font-size: 0.85rem; + cursor: pointer; +} + + +/* ----------------------------------------------------------------- + Collapsible Contribution Rows + ----------------------------------------------------------------- */ +.contribution-row { + background: white; + border: 1px solid #e0e0e0; + border-radius: 8px; + margin-bottom: 10px; + overflow: hidden; + transition: border-color 150ms ease; +} + +.contribution-row:hover { + border-color: #bbb; +} + +.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: 10px; + flex: 1; + min-width: 0; +} + +.contribution-row-summary .title { + font-weight: 600; + font-size: 0.95rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.collapse-icon { + color: #999; + font-size: 0.75rem; + flex-shrink: 0; + transition: transform 200ms ease; +} + +.contribution-row.open .collapse-icon { + transform: rotate(180deg); +} + + +/* ----------------------------------------------------------------- + Contribution Detail View (expanded) + ----------------------------------------------------------------- */ +.contribution-row-detail { + padding: 0 16px 16px 16px; + border-top: 1px solid #f0f0f0; + display: none; +} + +.contribution-row.open .contribution-row-detail { + display: block; +} + +.detail-layout { + display: flex; + gap: 16px; + margin-top: 12px; + margin-bottom: 12px; +} + +.detail-map { + width: 220px; + height: 170px; + 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: 10px; +} + +.detail-content .description.empty { + color: #bbb; + font-style: italic; +} + +.detail-meta { + font-size: 0.8rem; + color: #999; + display: flex; + flex-direction: column; + gap: 4px; +} + +.detail-meta span { + display: flex; + align-items: center; + gap: 6px; +} + + +/* ----------------------------------------------------------------- + Action Buttons + ----------------------------------------------------------------- */ +.action-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + padding-top: 12px; + border-top: 1px solid #f0f0f0; +} + +.btn { + padding: 7px 14px; + border: none; + border-radius: 6px; + font-size: 0.82rem; + font-weight: 600; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 5px; + font-family: inherit; + transition: filter 150ms ease; + text-decoration: none; +} + +.btn:hover { filter: brightness(1.1); } + +.btn-approve { background: #2e7d32; color: white; } +.btn-reject { background: #c62828; color: white; } +.btn-edit { background: #1565C0; color: white; } +.btn-delete { background: #424242; color: white; } +.btn-map { background: #546E7A; color: white; } + /* ----------------------------------------------------------------- Badges @@ -102,41 +328,13 @@ h2 { font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; + flex-shrink: 0; } .badge-pending { background: #fff3cd; color: #856404; } .badge-approved { background: #d4edda; color: #155724; } .badge-rejected { background: #f8d7da; color: #721c24; } -.badge-point { background: #e3f2fd; color: #1565c0; } -.badge-line { background: #f3e5f5; color: #6a1b9a; } -.badge-polygon { background: #e8f5e9; color: #2e7d32; } -/* ----------------------------------------------------------------- - Action Buttons - ----------------------------------------------------------------- */ -.action-buttons { - display: flex; - gap: 8px; - flex-shrink: 0; -} - -.btn { - padding: 8px 16px; - border: none; - border-radius: 6px; - font-size: 0.85rem; - font-weight: 600; - cursor: pointer; - display: inline-flex; - align-items: center; - gap: 4px; -} - -.btn-approve { background: #2e7d32; color: white; } -.btn-approve:hover { background: #1b5e20; } - -.btn-reject { background: #c62828; color: white; } -.btn-reject:hover { background: #b71c1c; } /* ----------------------------------------------------------------- Empty State @@ -148,6 +346,70 @@ h2 { font-size: 0.9rem; } + +/* ----------------------------------------------------------------- + Section Spacing + ----------------------------------------------------------------- */ +.section { margin-bottom: 32px; } + + +/* ----------------------------------------------------------------- + Placeholder Tabs (future Features) + ----------------------------------------------------------------- */ +.placeholder-content { + text-align: center; + padding: 60px 20px; + color: #bbb; +} + +.placeholder-content i { + font-size: 2.5rem; + margin-bottom: 12px; + display: block; +} + +.placeholder-content p { + font-size: 0.9rem; +} + + +/* ----------------------------------------------------------------- + Navigation Tabs (Page Sections) + ----------------------------------------------------------------- */ +.page-tabs { + display: flex; + gap: 4px; + margin-bottom: 24px; + background: white; + padding: 4px; + border-radius: 8px; + border: 1px solid #e0e0e0; +} + +.page-tab { + padding: 8px 16px; + border: none; + background: none; + font-family: inherit; + font-size: 0.85rem; + font-weight: 600; + color: #5a5a7a; + cursor: pointer; + border-radius: 6px; + transition: all 150ms ease; + display: flex; + align-items: center; + gap: 6px; +} + +.page-tab:hover { background: #f0f0f0; } + +.page-tab.active { + background: var(--color-primary); + color: white; +} + + /* ----------------------------------------------------------------- Login Page ----------------------------------------------------------------- */ @@ -186,7 +448,7 @@ h2 { border-radius: 6px; font-size: 0.9rem; margin-bottom: 12px; - font-family: 'Segoe UI', system-ui, sans-serif; + font-family: inherit; } .login-box input:focus { @@ -205,12 +467,12 @@ h2 { font-size: 0.9rem; font-weight: 600; cursor: pointer; - font-family: 'Segoe UI', system-ui, sans-serif; + font-family: inherit; } .login-box button:hover { filter: brightness(1.15); } -.error { +.login-error { color: #c62828; font-size: 0.85rem; margin-bottom: 12px; @@ -223,127 +485,19 @@ h2 { .back-link a { color: #5a5a7a; } + /* ----------------------------------------------------------------- Mobile Responsive ----------------------------------------------------------------- */ - .action-buttons { - width: 100%; - } - - .action-buttons form { - flex: 1; - } - - .action-buttons .btn { - 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) { + .admin-header { + flex-direction: column; + gap: 8px; + padding: 12px 16px; + } + + .admin-header h1 { font-size: 1rem; } + .detail-layout { flex-direction: column; } @@ -353,9 +507,23 @@ h2 { height: 180px; } - .contribution-row-summary { + .contribution-row-summary .title { + max-width: 200px; + } + + .action-buttons { flex-direction: column; - align-items: flex-start; - gap: 4px; + } + + .action-buttons .btn { + justify-content: center; + } + + .filter-tabs { + overflow-x: auto; + } + + .page-tabs { + overflow-x: auto; } } \ No newline at end of file diff --git a/public/admin.php b/public/admin.php index c60aa96..670f490 100644 --- a/public/admin.php +++ b/public/admin.php @@ -1,20 +1,26 @@ prepare("SELECT * FROM municipalities WHERE slug = :slug"); $stmt->execute([':slug' => 'lohne']); $municipality = $stmt->fetch(); -// Show Login Page if not authenticated +// Shows Login Page if not authenticated if ($page === 'login' || !is_admin()) { show_login_page($municipality, $login_error ?? null); exit; } // ----------------------------------------------------------------- -// Handle Moderation Actions (Approve / Reject) +// Loads shared Category Definitions // ----------------------------------------------------------------- -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mod_action'])) { - $contribution_id = $_POST['contribution_id'] ?? ''; - $mod_action = $_POST['mod_action']; - - if ($contribution_id && in_array($mod_action, ['approved', 'rejected'])) { - $stmt = $pdo->prepare("UPDATE contributions SET status = :status WHERE contribution_id = :id"); - $stmt->execute([':status' => $mod_action, ':id' => $contribution_id]); - } - - // Redirects to prevent Form Resubmission on Refresh - header('Location: admin.php'); - exit; -} +$categories = get_categories(); // ----------------------------------------------------------------- -// Load Contributions Data +// Loads Contributions and Statistics // ----------------------------------------------------------------- -// Pending Contributions +// All Contributions for this Municipality $stmt = $pdo->prepare(" - SELECT contribution_id, title, category, description, author_name, geom_type, status, created_at - FROM contributions - WHERE municipality_id = :mid AND status = 'pending' + SELECT contribution_id, title, category, description, author_name, + geom_type, status, likes_count, dislikes_count, created_at, updated_at + FROM contributions + WHERE municipality_id = :mid ORDER BY created_at DESC "); $stmt->execute([':mid' => $municipality['municipality_id']]); -$pending = $stmt->fetchAll(); +$all_contributions = $stmt->fetchAll(); -// Recently moderated Contributions -$stmt = $pdo->prepare(" - SELECT contribution_id, title, category, description, author_name, geom_type, status, created_at, updated_at - FROM contributions - WHERE municipality_id = :mid AND status IN ('approved', 'rejected') - ORDER BY updated_at DESC - LIMIT 20 -"); -$stmt->execute([':mid' => $municipality['municipality_id']]); -$moderated = $stmt->fetchAll(); - -// Statistics -$stmt = $pdo->prepare(" - SELECT status, COUNT(*) as count - FROM contributions - WHERE municipality_id = :mid - GROUP BY status -"); -$stmt->execute([':mid' => $municipality['municipality_id']]); -$stats_rows = $stmt->fetchAll(); -$stats = []; -foreach ($stats_rows as $row) { - $stats[$row['status']] = $row['count']; +// Counts per Status +$counts = ['pending' => 0, 'approved' => 0, 'rejected' => 0]; +foreach ($all_contributions as $item) { + if (isset($counts[$item['status']])) { + $counts[$item['status']]++; + } } - -// Category Labels for German Display -$category_labels = [ - 'mobility' => '🚲 Mobilität', - 'building' => '🏗️ Bauen', - 'energy' => '⚡ Energie', - 'environment' => '🌳 Umwelt', - 'industry' => '🏭 Industrie', - 'consumption' => '🛒 Konsum', - 'other' => '📌 Sonstiges' -]; +$counts['total'] = count($all_contributions); // ----------------------------------------------------------------- -// Render Main Page +// Renders Main Page // ----------------------------------------------------------------- ?> @@ -123,12 +91,18 @@ $category_labels = [ Moderation — <?= htmlspecialchars($municipality['name']) ?> + + + + + +

Moderation —

@@ -139,176 +113,275 @@ $category_labels = [
- -
-
-
-
Ausstehend
-
-
-
-
Freigegeben
-
-
-
-
Abgelehnt
-
-
-
-
Gesamt
-
+ + + +
+ + + +
- -
-

Ausstehende Beiträge ()

- -
- - Keine ausstehenden Beiträge. + + + +
+ + +
+
+
+
Ausstehend
- - -
-
-
- - ausstehend - - - - · - · - -
- -
-