change event
All checks were successful
Deploy / deploy (push) Successful in 54s

This commit is contained in:
2026-08-10 21:00:54 +02:00
parent d442b888a5
commit e0cc5989cc
13 changed files with 648 additions and 76 deletions

View File

@@ -12,6 +12,7 @@ if (!$userId) {
$communityCfg = require __DIR__ . '/../../../config/community.php';
$access = $pdo ? new \App\CommunityAccess($pdo, $communityCfg) : null;
$migration = $pdo ? new \App\CommunityMigration($pdo) : null;
$listingCatalog = $pdo ? new \App\ListingCatalog($pdo) : null;
if (!$access || !$access->canModerateForum((int)$userId)) {
http_response_code(403);
@@ -23,11 +24,18 @@ $error = '';
$info = '';
$canManageApplications = $access->canManageApplications((int)$userId) && $access->supportsApplications();
$canManageRoles = $access->canManageRoles((int)$userId);
$canReviewListings = $listingCatalog !== null && !$access->hasRole((int)$userId, 'owner');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = (string)($_POST['action'] ?? '');
try {
if ($action === 'application_decide') {
if ($action === 'listing_request_decide') {
if (!$canReviewListings || !$listingCatalog) {
throw new \RuntimeException('Keine Berechtigung für Listing-Freigaben.');
}
$listingCatalog->decideModerationRequest((int)$userId, (int)($_POST['request_id'] ?? 0), (string)($_POST['decision'] ?? ''), (string)($_POST['review_note'] ?? ''));
$info = 'Listing-Anfrage wurde bearbeitet.';
} elseif ($action === 'application_decide') {
$access->decideApplication((int)$userId, (int)($_POST['application_id'] ?? 0), (string)($_POST['decision'] ?? ''), (string)($_POST['decision_reason'] ?? ''));
$info = 'Bewerbung wurde bearbeitet.';
} elseif ($action === 'report_resolve') {
@@ -51,6 +59,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
$listingRequests = $canReviewListings && $listingCatalog ? $listingCatalog->listOpenModerationRequests() : [];
$applications = $canManageApplications ? $access->listApplications('open') : [];
$reports = $access->supportsReports() ? $access->listOpenReports() : [];
$roleAssignments = $canManageRoles ? $access->listRoleAssignments() : [];
@@ -84,6 +93,70 @@ $migrationStatus = ($migration && $canManageApplications) ? $migration->status()
<?php endif; ?>
<div class="forum-admin-grid">
<?php if ($canReviewListings): ?>
<section class="forum-board">
<div class="forum-board__head">
<div>
<h2>Offene Orts- und Veranstaltungsanfragen</h2>
<p class="muted">Neue Einträge sowie Änderungs- und Löschwünsche prüfen.</p>
</div>
</div>
<div class="forum-admin-list">
<?php foreach ($listingRequests as $request): ?>
<?php
$payload = is_array($request['payload'] ?? null) ? $request['payload'] : [];
$requestTypeLabel = match ((string)($request['request_type'] ?? '')) {
'create' => 'Neu',
'update' => 'Änderung',
'delete' => 'Löschung',
default => 'Anfrage',
};
?>
<article class="forum-admin-item">
<div>
<strong><?= htmlspecialchars((string)$requestTypeLabel, ENT_QUOTES) ?> · <?= htmlspecialchars((string)$request['title'], ENT_QUOTES) ?></strong>
<p class="muted small">Typ: <?= htmlspecialchars((string)(($request['listing_type'] ?? '') === 'place' ? 'Ort' : 'Veranstaltung'), ENT_QUOTES) ?> · Von <?= htmlspecialchars((string)($request['requested_by_name'] ?: 'Mitglied'), ENT_QUOTES) ?> · am <?= htmlspecialchars((string)$request['created_at'], ENT_QUOTES) ?></p>
<?php if (!empty($request['request_reason'])): ?>
<p><strong>Begründung:</strong><br><?= nl2br(htmlspecialchars((string)$request['request_reason'], ENT_QUOTES)) ?></p>
<?php endif; ?>
<p class="muted small">
<?= htmlspecialchars(trim(implode(' · ', array_filter([
trim(implode(', ', array_filter([
(string)($request['street'] ?? ''),
trim((string)($request['zip'] ?? '') . ' ' . (string)($request['city'] ?? '')),
]))),
(string)($request['region'] ?? ''),
]))), ENT_QUOTES) ?>
</p>
<?php if ($payload !== []): ?>
<div class="muted small" style="margin-top:8px;">
Vorgeschlagener Titel: <?= htmlspecialchars((string)($payload['title'] ?? $request['title']), ENT_QUOTES) ?><br>
Vorgeschlagene Adresse: <?= htmlspecialchars(trim(implode(', ', array_filter([
(string)($payload['street'] ?? ''),
trim((string)($payload['zip'] ?? '') . ' ' . (string)($payload['city'] ?? '')),
(string)($payload['region'] ?? ''),
]))), ENT_QUOTES) ?>
</div>
<?php endif; ?>
</div>
<form method="post" class="forum-admin-item__actions">
<input type="hidden" name="action" value="listing_request_decide">
<input type="hidden" name="request_id" value="<?= (int)$request['id'] ?>">
<textarea name="review_note" class="textarea" rows="3" placeholder="Hinweis für die Entscheidung"></textarea>
<div class="flex gap-12">
<button class="btn" type="submit" name="decision" value="approved">Freigeben</button>
<button class="btn ghost" type="submit" name="decision" value="rejected">Ablehnen</button>
</div>
</form>
</article>
<?php endforeach; ?>
<?php if (!$listingRequests): ?>
<div class="forum-empty">Keine offenen Orts- oder Veranstaltungsanfragen.</div>
<?php endif; ?>
</div>
</section>
<?php endif; ?>
<?php if ($canManageApplications): ?>
<section class="forum-board">
<div class="forum-board__head">

View File

@@ -18,8 +18,8 @@ if ($editingEvent && !empty($editEvent['starts_at'])) {
$startVal = date('Y-m-d', strtotime((string)$editListing['starts_at']));
}
$withChildrenValue = $editingEvent ? (((int)($editEvent['allow_kids'] ?? 1)) === 1 ? 'yes' : 'no') : (($editingListing && $currentEntryKind === 'editorial_event' && !empty($editListing['supports_registration'])) ? 'yes' : 'yes');
$eventTitle = $editingEvent ? 'Event bearbeiten' : ($editingListing ? ($currentEntryKind === 'place' ? 'Ort bearbeiten' : 'Veranstaltung bearbeiten') : ((($section ?? 'events') === 'places') ? 'Ausflugsziel anlegen' : 'Neues Event anlegen'));
$submitLabel = $editingEvent ? 'Event speichern' : ($editingListing ? ($currentEntryKind === 'place' ? 'Ort speichern' : 'Veranstaltung speichern') : ((($section ?? 'events') === 'places') ? 'Ausflugsziel anlegen' : 'Event anlegen'));
$eventTitle = $editingEvent ? 'Event bearbeiten' : ($editingListing ? ($currentEntryKind === 'place' ? 'Änderung für Ort anfragen' : 'Änderung für Veranstaltung anfragen') : ((($section ?? 'events') === 'places') ? 'Ort oder Veranstaltung anlegen' : 'Neues Event anlegen'));
$submitLabel = $editingEvent ? 'Event speichern' : ($editingListing ? 'Änderungsanfrage senden' : ((($section ?? 'events') === 'places') ? 'Eintrag zur Freigabe einreichen' : 'Event anlegen'));
$titleValue = $editingEvent ? (string)($editEvent['title'] ?? '') : (string)($editListing['title'] ?? '');
$teaserValue = $editingEvent ? (string)($editEvent['teaser_public'] ?? '') : (string)($editListing['teaser_public'] ?? '');
$descriptionValue = $editingEvent ? (string)($editEvent['description'] ?? '') : (string)($editListing['description'] ?? '');
@@ -87,7 +87,7 @@ $sectionLinks = [
'profile' => 'Profil',
'children' => 'Kinder',
'events' => 'Events',
'places' => 'Ausflugsziele',
'places' => 'Orte & Veranstaltungen',
'community' => 'Community',
'settings' => 'Einstellungen',
];
@@ -279,7 +279,7 @@ if (!empty($canManageSystemSettings)) {
<div class="account-panel__body">
<div class="account-inline-tabs" role="tablist" aria-label="Event-Bereiche">
<button class="account-inline-tabs__button is-active" type="button" role="tab" aria-selected="true" data-event-tab-trigger="my-events">Meine Events</button>
<button class="account-inline-tabs__button" type="button" role="tab" aria-selected="false" data-event-tab-trigger="sync">Synchronisation</button>
<button class="account-inline-tabs__button" type="button" role="tab" aria-selected="false" data-event-tab-trigger="sync">Kalendersynchronisation</button>
<button class="account-inline-tabs__button" type="button" role="tab" aria-selected="false" data-event-tab-trigger="past-events">Abgelaufene Events</button>
</div>
@@ -463,40 +463,94 @@ if (!empty($canManageSystemSettings)) {
<?php if ($section === 'places'): ?>
<section class="account-panel" id="places">
<div class="account-panel__head">
<h2>Ausflugsziele</h2>
<p class="muted">Hier pflegst du Orte und sonstige Veranstaltungen als mögliche Ausflugsziele.</p>
<h2>Orte &amp; Veranstaltungen</h2>
<p class="muted">Hier legst du dauerhafte Orte wie Cafés oder Spielplätze sowie zeitlich begrenzte Veranstaltungen an.</p>
</div>
<div class="account-panel__body">
<div class="flex gap-12" style="margin:0 0 16px 0; flex-wrap: wrap;">
<button class="btn" type="button" data-modal-open="modalEvent">Ausflugsziel anlegen</button>
<button class="btn" type="button" data-modal-open="modalEvent">Ort oder Veranstaltung anlegen</button>
</div>
<?php if (!$otherListings): ?>
<p class="muted small">Noch keine Orte oder Veranstaltungen angelegt.</p>
<div class="card" style="margin-bottom:18px;">
<strong>So ist dieser Bereich gedacht</strong>
<p class="muted small" style="margin:8px 0 0;">Orte sind feste, grundsätzlich verfügbare Ziele. Veranstaltungen sind zeitlich begrenzte Hinweise wie Kirmes, Aktionstage oder Pop-up-Angebote.</p>
<p class="muted small" style="margin:8px 0 0;">Neue Einträge werden erst nach Freigabe durch einen Admin sichtbar. Änderungs- und Löschwünsche laufen ebenfalls immer über eine begründete Anfrage.</p>
</div>
<div class="card" style="margin-bottom:18px;">
<strong>Deine eingereichten Vorschläge</strong>
<?php
$pendingListings = array_values(array_filter($userSubmittedListings ?? [], static function (array $entry): bool {
return (string)($entry['status'] ?? '') !== 'published';
}));
?>
<?php if (!$pendingListings): ?>
<p class="muted small" style="margin:8px 0 0;">Aktuell warten keine eigenen Orte oder Veranstaltungen auf Prüfung.</p>
<?php else: ?>
<ul class="dash-list" style="margin-top:12px;">
<?php foreach ($pendingListings as $entry): ?>
<li>
<strong><?= htmlspecialchars((string)$entry['title'], ENT_QUOTES) ?></strong>
<span class="badge"><?= ($entry['listing_type'] ?? '') === 'place' ? 'Ort' : 'Veranstaltung' ?></span>
<span class="badge" style="background:#fff7e6; color:#8a5a00;">
<?= (string)($entry['moderation_request_status'] ?? '') === 'rejected' ? 'Abgelehnt' : 'Wartet auf Freigabe' ?>
</span>
<?php if (!empty($entry['moderation_review_note'])): ?>
<div class="muted small" style="margin-top:6px;">Admin-Hinweis: <?= htmlspecialchars((string)$entry['moderation_review_note'], ENT_QUOTES) ?></div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php if (!$directoryListings): ?>
<p class="muted small">Noch keine freigegebenen Orte oder Veranstaltungen vorhanden.</p>
<?php else: ?>
<div class="card" style="margin-bottom:18px;">
<strong>Freigegebene Orte &amp; Veranstaltungen</strong>
<p class="muted small" style="margin:8px 0 0;">Hier siehst du die systemweit verfügbaren Einträge. Für Änderungen oder Löschungen ist immer eine begründete Anfrage nötig.</p>
</div>
<ul class="dash-list" style="margin-top:10px;">
<?php foreach ($otherListings as $entry): ?>
<?php foreach ($directoryListings as $entry): ?>
<?php $openRequestsForEntry = $listingRequestMap[(int)$entry['id']] ?? []; ?>
<li>
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap: wrap;">
<div style="display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap: wrap;">
<div>
<strong><?= htmlspecialchars((string)$entry['title'], ENT_QUOTES) ?></strong>
<span class="badge"><?= ($entry['listing_type'] ?? '') === 'place' ? 'Ort' : 'Veranstaltung' ?></span>
<?php if (!empty($entry['category_title'])): ?>
<span class="badge"><?= htmlspecialchars((string)$entry['category_title'], ENT_QUOTES) ?></span>
<?php endif; ?>
<?php if (in_array('update', $openRequestsForEntry, true)): ?>
<span class="badge" style="background:#fff7e6; color:#8a5a00;">Änderung angefragt</span>
<?php endif; ?>
<?php if (in_array('delete', $openRequestsForEntry, true)): ?>
<span class="badge" style="background:#fee2e2; color:#991b1b;">Löschung angefragt</span>
<?php endif; ?>
<div class="muted small" style="margin-top:4px;">
<?= htmlspecialchars(trim(implode(' · ', array_filter([
(string)($entry['city'] ?? ''),
trim(implode(', ', array_filter([
(string)($entry['street'] ?? ''),
trim((string)($entry['zip'] ?? '') . ' ' . (string)($entry['city'] ?? '')),
]))),
(string)($entry['region'] ?? ''),
!empty($entry['starts_at']) ? (string)$entry['starts_at'] : null,
]))), ENT_QUOTES) ?>
</div>
</div>
<div class="flex gap-8" style="flex-wrap: wrap;">
<a class="btn ghost" href="/dashboard?section=places&edit_listing=<?= (int)$entry['id'] ?>#places">Bearbeiten</a>
<form method="post" action="/dashboard?section=places#places" onsubmit="return confirm('Eintrag wirklich löschen?');">
<input type="hidden" name="action" value="listing_delete">
<div class="stack gap-8" style="min-width:min(100%, 320px);">
<div class="flex gap-8" style="flex-wrap: wrap;">
<?php if (!in_array('update', $openRequestsForEntry, true)): ?>
<a class="btn ghost" href="/dashboard?section=places&edit_listing=<?= (int)$entry['id'] ?>#places">Änderung anfragen</a>
<?php endif; ?>
</div>
<?php if (!in_array('delete', $openRequestsForEntry, true)): ?>
<form method="post" action="/dashboard?section=places#places" class="stack gap-6">
<input type="hidden" name="action" value="listing_delete_request">
<input type="hidden" name="listing_id" value="<?= (int)$entry['id'] ?>">
<button class="btn ghost" type="submit">Löschen</button>
<label class="label" for="deleteReason<?= (int)$entry['id'] ?>">Löschanfrage begründen</label>
<textarea id="deleteReason<?= (int)$entry['id'] ?>" name="moderation_reason" class="textarea" rows="2" placeholder="Warum sollte dieser Eintrag entfernt werden?" required></textarea>
<button class="btn ghost" type="submit">Löschung anfragen</button>
</form>
<?php endif; ?>
</div>
</div>
</li>
@@ -1131,6 +1185,15 @@ if (!empty($canManageSystemSettings)) {
<option value="weekly" <?= $recurrenceModeValue === 'weekly' ? 'selected' : '' ?>>Wöchentlich</option>
</select>
</div>
<?php if ($editingListing): ?>
<div class="stack gap-6">
<label class="label" for="listingModerationReason">Begründung für die Änderungsanfrage</label>
<textarea id="listingModerationReason" name="moderation_reason" class="textarea" rows="3" placeholder="Was stimmt nicht oder was soll angepasst werden?" required></textarea>
<p class="muted small" style="margin:0;">Die Änderung wird erst nach Prüfung durch einen Admin freigegeben.</p>
</div>
<?php elseif (($section ?? '') === 'places'): ?>
<p class="muted small" style="margin:0;">Neue Orte und Veranstaltungen werden erst nach Prüfung durch einen Admin freigegeben.</p>
<?php endif; ?>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="evZip">PLZ</label>