Files
webgis-lohne/public/admin.php

586 lines
30 KiB
PHP

<?php
// =====================================================================
// Moderation Page
// Lists Contributions for Review. Moderators can approve, reject,
// edit and delete Contributions. Includes Map Preview and Filtering.
// =====================================================================
// 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 all Comments with Contribution Titles for Moderation
$stmt = $pdo->prepare("
SELECT cm.comment_id, cm.contribution_id, cm.author_name, cm.browser_id,
cm.content, cm.status, cm.created_at,
co.title AS contribution_title, co.category AS contribution_category
FROM comments cm
JOIN contributions co ON cm.contribution_id = co.contribution_id
WHERE co.municipality_id = :mid
ORDER BY cm.created_at DESC
");
$stmt->execute([':mid' => $municipality['municipality_id']]);
$all_comments = $stmt->fetchAll();
// Counts Comments per Status
$comment_counts = ['pending' => 0, 'approved' => 0, 'rejected' => 0];
foreach ($all_comments as $c) {
if (isset($comment_counts[$c['status']])) {
$comment_counts[$c['status']]++;
}
}
$comment_counts['total'] = count($all_comments);
// 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, comment_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="assets/shield-halved-solid-off-black.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">
<!-- 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'] ?>
&middot;
<i class="fa-solid fa-thumbs-down"></i> <?= $item['dislikes_count'] ?>
&middot;
<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(<?= $item['contribution_id'] ?>, '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;">
<!-- Status Filter Tabs for Comments -->
<div class="filter-tabs" id="comment-filter-tabs">
<button class="filter-tab active" onclick="filterCommentsByStatus('all', this)">
Alle <span class="tab-count"><?= $comment_counts['total'] ?></span>
</button>
<button class="filter-tab" onclick="filterCommentsByStatus('pending', this)">
Ausstehend <span class="tab-count"><?= $comment_counts['pending'] ?></span>
</button>
<button class="filter-tab" onclick="filterCommentsByStatus('approved', this)">
Akzeptiert <span class="tab-count"><?= $comment_counts['approved'] ?></span>
</button>
<button class="filter-tab" onclick="filterCommentsByStatus('rejected', this)">
Abgelehnt <span class="tab-count"><?= $comment_counts['rejected'] ?></span>
</button>
</div>
<!-- Sort Controls -->
<div class="sort-controls">
<span id="comment-visible-count"><?= $comment_counts['total'] ?> 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):
$comment_cat = $categories[$comment['contribution_category'] ?? ''] ?? ['label' => 'Unbekannt', 'faIcon' => 'fa-question', 'color' => '#999'];
$comment_status_label = ['pending' => 'Ausstehend', 'approved' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
?>
<div class="contribution-row comment-mod-row"
data-status="<?= $comment['status'] ?>"
data-date="<?= $comment['created_at'] ?>"
data-contribution="<?= htmlspecialchars($comment['contribution_title']) ?>">
<!-- Collapsed: Contribution Title + Comment Status + Category -->
<div class="contribution-row-header" onclick="toggleRow(this.parentElement)">
<div class="contribution-row-summary">
<span class="title"><?= htmlspecialchars($comment['contribution_title']) ?></span>
<span class="badge badge-<?= $comment['status'] ?>"><?= $comment_status_label[$comment['status']] ?? $comment['status'] ?></span>
<span class="badge badge-category">
<i class="fa-solid <?= $comment_cat['faIcon'] ?>"></i>
<?= $comment_cat['label'] ?>
</span>
</div>
<i class="fa-solid fa-chevron-down collapse-icon"></i>
</div>
<!-- Expanded Detail -->
<div class="contribution-row-detail">
<div style="padding:12px 0;">
<!-- Comment Content -->
<div style="font-size:0.9rem;line-height:1.6;color:var(--color-text);margin-bottom:12px;">
<?= 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">
<?php if ($comment['status'] !== 'approved'): ?>
<button class="btn btn-approve" onclick="changeCommentStatus(<?= $comment['comment_id'] ?>, 'approved')">
<i class="fa-solid fa-check"></i> Akzeptieren
</button>
<?php endif; ?>
<?php if ($comment['status'] !== 'rejected'): ?>
<button class="btn btn-reject" onclick="changeCommentStatus(<?= $comment['comment_id'] ?>, 'rejected')">
<i class="fa-solid fa-xmark"></i> Ablehnen
</button>
<?php endif; ?>
<?php if ($comment['status'] !== 'pending'): ?>
<button class="btn btn-reset" onclick="changeCommentStatus(<?= $comment['comment_id'] ?>, 'pending')">
<i class="fa-solid fa-rotate-left"></i> Zurücksetzen
</button>
<?php endif; ?>
<button class="btn btn-edit" onclick="editModComment(<?= $comment['comment_id'] ?>, '<?= htmlspecialchars(addslashes($comment['content']), ENT_QUOTES) ?>')">
<i class="fa-solid fa-pen"></i> Bearbeiten
</button>
<button class="btn btn-delete" onclick="deleteModComment(<?= $comment['comment_id'] ?>)">
<i class="fa-solid fa-trash"></i> 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>
<!-- ============================================================= -->
<!-- Loads JavaScript Dependencies -->
<!-- ============================================================= -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<!-- ============================================================= -->
<!-- Admin Configuration passed to JavaScript -->
<!-- ============================================================= -->
<script>
const ADMIN_CONFIG = {
id: <?= $municipality['municipality_id'] ?>,
name: "<?= htmlspecialchars($municipality['name'], ENT_QUOTES) ?>",
slug: "<?= htmlspecialchars($municipality['slug'], ENT_QUOTES) ?>",
center: [<?= $municipality['center_lat'] ?>, <?= $municipality['center_lng'] ?>],
zoom: <?= $municipality['default_zoom'] ?>,
primaryColor: "<?= htmlspecialchars($municipality['primary_color'], ENT_QUOTES) ?>"
};
</script>
<!-- Application Logic -->
<script src="js/admin.js"></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> <a href="index.php">Zurück zum Bürgerportal</a></div>
</div>
</div>
</body>
</html>
<?php
}
?>