1098 lines
51 KiB
PHP
1098 lines
51 KiB
PHP
<?php
|
|
// =====================================================================
|
|
// Moderation Page
|
|
// Lists Contributions for Review. Moderators can approve, reject,
|
|
// edit and delete Contributions. Includes Map Preview and Filtering.
|
|
//
|
|
// ToDo's:
|
|
// - Comment Moderation Tab
|
|
// - News Management Tab
|
|
// - User Management Tab
|
|
// - Analytics Tab
|
|
// =====================================================================
|
|
|
|
// Reads Environment Configfile
|
|
$envFile = __DIR__ . '/../../.env';
|
|
if (file_exists($envFile)) {
|
|
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
foreach ($lines as $line) {
|
|
if (strpos(trim($line), '#') === 0) continue;
|
|
list($key, $value) = array_map('trim', explode('=', $line, 2));
|
|
putenv("$key=$value");
|
|
}
|
|
}
|
|
|
|
require_once __DIR__ . '/api/db.php';
|
|
require_once __DIR__ . '/api/auth.php';
|
|
|
|
|
|
// -----------------------------------------------------------------
|
|
// Routing: Login, Logout, or Main Page
|
|
// -----------------------------------------------------------------
|
|
$page = $_GET['page'] ?? 'main';
|
|
|
|
// Handles Login
|
|
if ($page === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$password = $_POST['password'] ?? '';
|
|
if (admin_login($password)) {
|
|
header('Location: admin.php');
|
|
exit;
|
|
} else {
|
|
$login_error = 'Falsches Passwort.';
|
|
}
|
|
}
|
|
|
|
// Handles Logout
|
|
if ($page === 'logout') {
|
|
admin_logout();
|
|
header('Location: admin.php?page=login');
|
|
exit;
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Loads Municipality Configuration for Theming
|
|
// -----------------------------------------------------------------
|
|
$pdo = get_db();
|
|
$stmt = $pdo->prepare("SELECT * FROM municipalities WHERE slug = :slug");
|
|
$stmt->execute([':slug' => getenv('MUNICIPALITY_SLUG')]);
|
|
$municipality = $stmt->fetch();
|
|
|
|
|
|
// Loads News for Moderation
|
|
$stmt = $pdo->prepare("
|
|
SELECT news_id, title, content, author_name, published_at, created_at
|
|
FROM news
|
|
WHERE municipality_id = :mid
|
|
ORDER BY published_at DESC
|
|
");
|
|
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
|
$news_items = $stmt->fetchAll();
|
|
|
|
|
|
// Loads Comments with Contribution for Moderation
|
|
$stmt = $pdo->prepare("
|
|
SELECT contribution_id, title, category, description, author_name,
|
|
geom_type, status, likes_count, dislikes_count, comment_count,
|
|
photo_path, created_at, updated_at
|
|
FROM contributions
|
|
WHERE municipality_id = :mid
|
|
ORDER BY created_at DESC
|
|
");
|
|
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
|
$all_comments = $stmt->fetchAll();
|
|
|
|
|
|
// Shows Login Page if not authenticated
|
|
if ($page === 'login' || !is_admin()) {
|
|
show_login_page($municipality, $login_error ?? null);
|
|
exit;
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Loads shared Category Definitions
|
|
// -----------------------------------------------------------------
|
|
$categories = get_categories();
|
|
|
|
// -----------------------------------------------------------------
|
|
// Loads Contributions and Statistics
|
|
// -----------------------------------------------------------------
|
|
|
|
// Loads all Contributions for Municipality
|
|
$stmt = $pdo->prepare("
|
|
SELECT contribution_id, title, category, description, author_name, photo_path,
|
|
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']]);
|
|
$all_contributions = $stmt->fetchAll();
|
|
|
|
// Counts per Status
|
|
$counts = ['pending' => 0, 'approved' => 0, 'rejected' => 0];
|
|
foreach ($all_contributions as $item) {
|
|
if (isset($counts[$item['status']])) {
|
|
$counts[$item['status']]++;
|
|
}
|
|
}
|
|
$counts['total'] = count($all_contributions);
|
|
|
|
// -----------------------------------------------------------------
|
|
// Renders Main Page
|
|
// -----------------------------------------------------------------
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Moderation — <?= htmlspecialchars($municipality['name']) ?></title>
|
|
<link rel="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.png') ?>" type="image/png">
|
|
|
|
<!-- Loads CSS Dependencies -->
|
|
|
|
<!-- Font Awesome for Icons -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
|
|
<!-- Leaflet -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css">
|
|
|
|
<!-- Application Styles -->
|
|
<link rel="stylesheet" href="styles.css">
|
|
|
|
|
|
<!-- Loads JavaScript Dependencies -->
|
|
|
|
<!-- SweetAlert2 -->
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.14.0/dist/sweetalert2.all.min.js"></script>
|
|
|
|
|
|
<!-- Loads Municipality Theme from Database -->
|
|
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ============================================================= -->
|
|
<!-- Header -->
|
|
<!-- ============================================================= -->
|
|
<div class="page-header">
|
|
<div class="page-header-inner">
|
|
<h1><i class="fa-solid fa-shield-halved"></i> Moderationsportal <?= htmlspecialchars($municipality['name']) ?></h1>
|
|
<div class="page-header-nav">
|
|
<a href="index.php"><i class="fa-solid fa-map"></i> Bürgerportal</a>
|
|
<a href="admin.php?page=logout"><i class="fa-solid fa-right-from-bracket"></i> Abmelden</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-container">
|
|
|
|
<!-- ========================================================= -->
|
|
<!-- Page Navigation Tabs -->
|
|
<!-- ========================================================= -->
|
|
<div class="page-tabs">
|
|
<button class="page-tab active" onclick="showPageTab('contributions')">
|
|
<i class="fa-solid fa-list-check"></i> Beiträge
|
|
</button>
|
|
<button class="page-tab" onclick="showPageTab('comments')">
|
|
<i class="fa-solid fa-comments"></i> Kommentare
|
|
</button>
|
|
<button class="page-tab" onclick="showPageTab('news')">
|
|
<i class="fa-solid fa-newspaper"></i> Neuigkeiten
|
|
</button>
|
|
<button class="page-tab" onclick="showPageTab('stats')">
|
|
<i class="fa-solid fa-chart-bar"></i> Statistik
|
|
</button>
|
|
<button class="page-tab" onclick="showPageTab('users')">
|
|
<i class="fa-solid fa-users"></i> Benutzer
|
|
</button>
|
|
</div>
|
|
|
|
|
|
<!-- ========================================================= -->
|
|
<!-- Contributions Tab -->
|
|
<!-- ========================================================= -->
|
|
<div id="tab-contributions" class="page-tab-content">
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-number"><?= $counts['total'] ?></div>
|
|
<div class="stat-label">Alle</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number"><?= $counts['pending'] ?></div>
|
|
<div class="stat-label">Ausstehend</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number"><?= $counts['approved'] ?></div>
|
|
<div class="stat-label">Akzeptiert</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number"><?= $counts['rejected'] ?></div>
|
|
<div class="stat-label">Abgelehnt</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Status Filter Tabs -->
|
|
<div class="filter-tabs">
|
|
<button class="filter-tab active" onclick="filterByStatus('all', this)">
|
|
Alle <span class="tab-count"><?= $counts['total'] ?></span>
|
|
</button>
|
|
<button class="filter-tab" onclick="filterByStatus('pending', this)">
|
|
Ausstehend <span class="tab-count"><?= $counts['pending'] ?></span>
|
|
</button>
|
|
<button class="filter-tab" onclick="filterByStatus('approved', this)">
|
|
Akzeptiert <span class="tab-count"><?= $counts['approved'] ?></span>
|
|
</button>
|
|
<button class="filter-tab" onclick="filterByStatus('rejected', this)">
|
|
Abgelehnt <span class="tab-count"><?= $counts['rejected'] ?></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Sort Controls -->
|
|
<div class="sort-controls">
|
|
<span id="visible-count"><?= $counts['total'] ?> Beiträge</span>
|
|
<select onchange="sortContributions(this.value)">
|
|
<option value="date-desc">Neueste zuerst</option>
|
|
<option value="date-asc">Älteste zuerst</option>
|
|
<option value="category">Nach Kategorie</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Contribution List -->
|
|
<div id="contributions-container">
|
|
<?php if (empty($all_contributions)): ?>
|
|
<div class="empty-state">
|
|
<i class="fa-solid fa-inbox" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
|
|
Noch keine Beiträge vorhanden.
|
|
</div>
|
|
<?php else: ?>
|
|
<?php foreach ($all_contributions as $item):
|
|
$cat = $categories[$item['category']] ?? ['label' => $item['category'], 'faIcon' => 'fa-question', 'color' => '#999'];
|
|
$status_label = ['pending' => 'Ausstehend', 'approved' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
|
|
?>
|
|
<div class="contribution-row"
|
|
data-status="<?= $item['status'] ?>"
|
|
data-category="<?= htmlspecialchars($item['category']) ?>"
|
|
data-date="<?= $item['created_at'] ?>"
|
|
data-id="<?= $item['contribution_id'] ?>">
|
|
|
|
<!-- Collapsed Header: Title + Status -->
|
|
<div class="contribution-row-header" onclick="toggleRow(this.parentElement)">
|
|
<div class="contribution-row-summary">
|
|
<span class="title"><?= htmlspecialchars($item['title']) ?></span>
|
|
<span class="badge badge-category">
|
|
<i class="fa-solid <?= $cat['faIcon'] ?>"></i>
|
|
<?= $cat['label'] ?>
|
|
</span>
|
|
<span class="badge badge-<?= $item['status'] ?>"><?= $status_label[$item['status']] ?? $item['status'] ?></span>
|
|
</div>
|
|
<i class="fa-solid fa-chevron-down collapse-icon"></i>
|
|
</div>
|
|
|
|
<!-- Expanded Detail -->
|
|
<div class="contribution-row-detail">
|
|
<div class="detail-layout">
|
|
<!-- Map and Photo Slider -->
|
|
<div class="detail-slider" id="slider-<?= $item['contribution_id'] ?>">
|
|
<!-- Slide 1: Map -->
|
|
<div class="detail-slide active" data-slide="map">
|
|
<div class="detail-map" id="map-<?= $item['contribution_id'] ?>"
|
|
data-contribution-id="<?= $item['contribution_id'] ?>">
|
|
</div>
|
|
</div>
|
|
<?php if (!empty($item['photo_path'])): ?>
|
|
<!-- Slide 2: Photo -->
|
|
<div class="detail-slide" data-slide="photo" style="display:none;">
|
|
<img src="<?= htmlspecialchars($item['photo_path']) ?>" alt="Foto"
|
|
class="detail-slide-photo" onclick="window.open('<?= htmlspecialchars($item['photo_path']) ?>', '_blank')">
|
|
</div>
|
|
<!-- Slider Arrows -->
|
|
<button class="slider-arrow slider-arrow-left" onclick="slideDetail(<?= $item['contribution_id'] ?>, -1)">
|
|
<i class="fa-solid fa-chevron-left"></i>
|
|
</button>
|
|
<button class="slider-arrow slider-arrow-right" onclick="slideDetail(<?= $item['contribution_id'] ?>, 1)">
|
|
<i class="fa-solid fa-chevron-right"></i>
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="detail-content">
|
|
<?php if ($item['description']): ?>
|
|
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
|
|
<?php else: ?>
|
|
<div class="description empty">Keine Beschreibung vorhanden.</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="detail-meta">
|
|
<span><i class="fa-solid fa-user"></i> <?= htmlspecialchars($item['author_name']) ?></span>
|
|
<span><i class="fa-solid fa-calendar"></i> <?= date('d.m.Y, H:i', strtotime($item['created_at'])) ?> Uhr</span>
|
|
<span>
|
|
<i class="fa-solid fa-thumbs-up"></i> <?= $item['likes_count'] ?>
|
|
·
|
|
<i class="fa-solid fa-thumbs-down"></i> <?= $item['dislikes_count'] ?>
|
|
·
|
|
<i class="fa-solid fa-comment"></i> <?= $item['comment_count'] ?? 0 ?>
|
|
|
|
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="action-buttons">
|
|
<?php if ($item['status'] !== 'approved'): ?>
|
|
<button class="btn btn-approve" onclick="changeStatus(<?= $item['contribution_id'] ?>, 'approved')">
|
|
<i class="fa-solid fa-check"></i> Akzeptieren
|
|
</button>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($item['status'] !== 'rejected'): ?>
|
|
<button class="btn btn-reject" onclick="changeStatus(<?= $item['contribution_id'] ?>, 'rejected')">
|
|
<i class="fa-solid fa-xmark"></i> Ablehnen
|
|
</button>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($item['status'] !== 'pending'): ?>
|
|
<button class="btn btn-reset" onclick="changeStatus(..., 'pending')">
|
|
<i class="fa-solid fa-rotate-left"></i> Zurücksetzen
|
|
</button>
|
|
<?php endif; ?>
|
|
|
|
<button class="btn btn-edit" onclick="editContribution(<?= $item['contribution_id'] ?>, '<?= htmlspecialchars(addslashes($item['title']), ENT_QUOTES) ?>', '<?= htmlspecialchars(addslashes($item['description'] ?? ''), ENT_QUOTES) ?>')">
|
|
<i class="fa-solid fa-pen"></i> Bearbeiten
|
|
</button>
|
|
|
|
<button class="btn btn-delete" onclick="deleteContribution(<?= $item['contribution_id'] ?>)">
|
|
<i class="fa-solid fa-trash"></i> Löschen
|
|
</button>
|
|
|
|
<a class="btn btn-map" href="index.php" target="_blank">
|
|
<i class="fa-solid fa-map-location-dot"></i> Karte
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ========================================================= -->
|
|
<!-- Comments Moderation Tab -->
|
|
<!-- ========================================================= -->
|
|
<div id="tab-comments" class="page-tab-content" style="display:none;">
|
|
|
|
<!-- Sort Controls -->
|
|
<div class="sort-controls">
|
|
<span><?= count($all_comments) ?> Kommentare</span>
|
|
<select onchange="sortCommentRows(this.value)">
|
|
<option value="date-desc">Neueste zuerst</option>
|
|
<option value="date-asc">Älteste zuerst</option>
|
|
<option value="contribution">Nach Beitrag</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Comments List -->
|
|
<div id="comments-mod-container">
|
|
<?php if (empty($all_comments)): ?>
|
|
<div class="empty-state">
|
|
<i class="fa-solid fa-comments" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
|
|
Noch keine Kommentare vorhanden.
|
|
</div>
|
|
<?php else: ?>
|
|
<?php foreach ($all_comments as $comment): ?>
|
|
<div class="contribution-row comment-mod-row"
|
|
data-date="<?= $comment['created_at'] ?>"
|
|
data-contribution="<?= htmlspecialchars($comment['contribution_title']) ?>">
|
|
<div class="contribution-row-header" onclick="toggleRow(this.parentElement)">
|
|
<div class="contribution-row-summary">
|
|
<span class="title" style="flex:1;"><?= htmlspecialchars(mb_strimwidth($comment['content'], 0, 80, '...')) ?></span>
|
|
<span style="font-size:0.75rem;color:#999;">
|
|
<?= htmlspecialchars($comment['author_name']) ?>
|
|
· <?= date('d.m.Y H:i', strtotime($comment['created_at'])) ?>
|
|
</span>
|
|
</div>
|
|
<i class="fa-solid fa-chevron-down collapse-icon"></i>
|
|
</div>
|
|
<div class="contribution-row-detail">
|
|
<div style="padding:12px 0;">
|
|
<!-- Reference to Contribution -->
|
|
<div style="font-size:0.8rem;color:var(--color-text-secondary);margin-bottom:8px;padding:8px 12px;background:#f8f9fa;border-radius:6px;border-left:3px solid var(--color-primary);">
|
|
<i class="fa-solid fa-reply"></i> Beitrag: <strong><?= htmlspecialchars($comment['contribution_title']) ?></strong>
|
|
</div>
|
|
<!-- Comment Content -->
|
|
<div style="font-size:0.9rem;line-height:1.6;color:var(--color-text);margin-bottom:8px;">
|
|
<?= nl2br(htmlspecialchars($comment['content'])) ?>
|
|
</div>
|
|
<!-- Meta -->
|
|
<div class="detail-meta">
|
|
<span><i class="fa-solid fa-user"></i> <?= htmlspecialchars($comment['author_name']) ?></span>
|
|
<span><i class="fa-solid fa-calendar"></i> <?= date('d.m.Y, H:i', strtotime($comment['created_at'])) ?> Uhr</span>
|
|
</div>
|
|
</div>
|
|
<!-- Action Buttons -->
|
|
<div class="action-buttons">
|
|
<button class="btn btn-delete" onclick="deleteModComment(<?= $comment['comment_id'] ?>)">
|
|
<i class="fa-solid fa-trash"></i> Kommentar löschen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ========================================================= -->
|
|
<!-- News Article Tab -->
|
|
<!-- ========================================================= -->
|
|
<div id="tab-news" class="page-tab-content" style="display:none;">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
|
|
<h2 style="margin:0;border:none;padding:0;"><i class="fa-solid fa-newspaper"></i> Neuigkeiten</h2>
|
|
<button class="btn btn-approve" onclick="createNews()">
|
|
<i class="fa-solid fa-plus"></i> Nachricht hinzufügen
|
|
</button>
|
|
</div>
|
|
|
|
<?php if (empty($news_items)): ?>
|
|
<div class="empty-state">
|
|
<i class="fa-solid fa-newspaper" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
|
|
Noch keine Neuigkeiten veröffentlicht.
|
|
</div>
|
|
<?php else: ?>
|
|
<?php foreach ($news_items as $news): ?>
|
|
<div class="contribution-row" data-id="<?= $news['news_id'] ?>">
|
|
<div class="contribution-row-header" onclick="toggleRow(this.parentElement)">
|
|
<div class="contribution-row-summary">
|
|
<span class="title"><?= htmlspecialchars($news['title']) ?></span>
|
|
<span style="font-size:0.8rem;color:#999;">
|
|
<?= date('d.m.Y', strtotime($news['published_at'])) ?>
|
|
· <?= htmlspecialchars($news['author_name']) ?>
|
|
</span>
|
|
</div>
|
|
<i class="fa-solid fa-chevron-down collapse-icon"></i>
|
|
</div>
|
|
<div class="contribution-row-detail">
|
|
<div style="padding:12px 0;font-size:0.9rem;line-height:1.6;color:#5a5a7a;">
|
|
<?= nl2br(htmlspecialchars($news['content'])) ?>
|
|
</div>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-edit" onclick="editNews(<?= $news['news_id'] ?>, '<?= htmlspecialchars(addslashes($news['title']), ENT_QUOTES) ?>', '<?= htmlspecialchars(addslashes($news['content']), ENT_QUOTES) ?>', '<?= htmlspecialchars(addslashes($news['author_name']), ENT_QUOTES) ?>')">
|
|
<i class="fa-solid fa-pen"></i> Bearbeiten
|
|
</button>
|
|
<button class="btn btn-delete" onclick="deleteNews(<?= $news['news_id'] ?>)">
|
|
<i class="fa-solid fa-trash"></i> Löschen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
|
|
<!-- ========================================================= -->
|
|
<!-- Placeholder Tabs for future Features -->
|
|
<!-- ========================================================= -->
|
|
<div id="tab-stats" class="page-tab-content" style="display:none;">
|
|
<div class="placeholder-content">
|
|
<i class="fa-solid fa-chart-bar"></i>
|
|
<p>Statistiken und Analysen - geplant in zukünftiger Version.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-users" class="page-tab-content" style="display:none;">
|
|
<div class="placeholder-content">
|
|
<i class="fa-solid fa-users"></i>
|
|
<p>Benutzerverwaltung - geplant in zukünftiger Version.</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<!-- ============================================================= -->
|
|
<!-- JavaScript: Leaflet, Interactions, API Calls -->
|
|
<!-- ============================================================= -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
|
|
|
<script>
|
|
// Municipality Configuration for Map Previews
|
|
const MUNICIPALITY_CENTER = [<?= $municipality['center_lat'] ?>, <?= $municipality['center_lng'] ?>];
|
|
const MUNICIPALITY_ID = <?= $municipality['municipality_id'] ?>;
|
|
const API_URL = 'api/contributions.php';
|
|
const PRIMARY_COLOR = '<?= htmlspecialchars($municipality['primary_color']) ?>';
|
|
|
|
// Current Status Filter
|
|
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(); });
|
|
});
|
|
});
|
|
}
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
|
|
|
|
<?php
|
|
// -----------------------------------------------------------------
|
|
// Login Page
|
|
// -----------------------------------------------------------------
|
|
function show_login_page($municipality, $error = null) {
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Moderation - Anmeldung</title>
|
|
<link rel="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.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="styles.css">
|
|
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
|
|
</head>
|
|
<body>
|
|
<div class="login-wrapper">
|
|
<div class="login-box">
|
|
<h1><i class="fa-solid fa-shield-halved"></i> Moderationsportal</h1>
|
|
<p>Bitte geben Sie das Moderationspasswort ein.</p>
|
|
<?php if ($error): ?>
|
|
<div class="login-error"><i class="fa-solid fa-triangle-exclamation"></i> <?= htmlspecialchars($error) ?></div>
|
|
<?php endif; ?>
|
|
<form method="POST" action="admin.php?page=login">
|
|
<input type="password" name="password" placeholder="Passwort" autofocus>
|
|
<button type="submit"><i class="fa-solid fa-right-to-bracket"></i> Anmelden</button>
|
|
</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>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
}
|
|
?>
|