sadasd
All checks were successful
Deploy / deploy (push) Successful in 56s

This commit is contained in:
2026-08-19 20:04:01 +02:00
parent c2eaa5c186
commit c567d19deb
13 changed files with 177 additions and 38 deletions

View File

@@ -51,6 +51,8 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- System-Level bleiben getrennt als Rollen `Forum-Admin`, `Site-Admin` und `SiteOwner`. - System-Level bleiben getrennt als Rollen `Forum-Admin`, `Site-Admin` und `SiteOwner`.
- Die Vergabe von System-Leveln läuft im Bereich `Community-Admin` jetzt über eine Benutzersuche statt über rohe Benutzer-IDs; `Site-Admins` bleiben auf die bestehende Rollenlogik begrenzt. - Die Vergabe von System-Leveln läuft im Bereich `Community-Admin` jetzt über eine Benutzersuche statt über rohe Benutzer-IDs; `Site-Admins` bleiben auf die bestehende Rollenlogik begrenzt.
- Kategorien liegen nicht mehr unter `System`, sondern in einem eigenen Mitgliederbereichspunkt `Kategorien`; dort werden nur neue Kategorien angezeigt und per Vorschlagsfeld mit bestehenden Kategorien aus der Datenbank zusammengeführt. - Kategorien liegen nicht mehr unter `System`, sondern in einem eigenen Mitgliederbereichspunkt `Kategorien`; dort werden nur neue Kategorien angezeigt und per Vorschlagsfeld mit bestehenden Kategorien aus der Datenbank zusammengeführt.
- Neue Kategorien bleiben dort sichtbar, bis sie von einem Berechtigten bestätigt oder mit einer bestehenden Kategorie zusammengeführt werden.
- Community-Level können zusätzlich die Rechte für Kategorien sowie für die Freigabe von Orten und Veranstaltungen tragen.
## Technischer Rahmen ## Technischer Rahmen
- eigener Front-Controller in `public/index.php` - eigener Front-Controller in `public/index.php`

View File

@@ -25,6 +25,7 @@ Stand: 2026-08-10
- Kalender-Export und abonnierbarer Feed: `src/App/CalendarSync.php` plus `public/page/calendar/export.php` und `public/page/calendar/feed.php` - Kalender-Export und abonnierbarer Feed: `src/App/CalendarSync.php` plus `public/page/calendar/export.php` und `public/page/calendar/feed.php`
- UI für getrennte Bereiche `Events` und `Orte & Veranstaltungen` im Mitgliederbereich: `partials/landing/account/dashboard.php` - UI für getrennte Bereiche `Events` und `Orte & Veranstaltungen` im Mitgliederbereich: `partials/landing/account/dashboard.php`
- eigener Mitgliederbereichspunkt `Kategorien` für neue Kategorien und deren Zusammenführung per Vorschlagsfeld: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php` - eigener Mitgliederbereichspunkt `Kategorien` für neue Kategorien und deren Zusammenführung per Vorschlagsfeld: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php`
- Community-Level-Rechte für Kategorien und Orts-/Veranstaltungsfreigaben: `config/community.php`, `src/App/Community.php`, `src/App/CommunityAccess.php`, `partials/landing/account/community-admin.php`
- Preis-, Bild- und Ortseingabe für Orte und Veranstaltungen sowie reduzierte Event-Maske: `partials/landing/account/dashboard.php` plus Speicherung in `src/App/ListingCatalog.php` - Preis-, Bild- und Ortseingabe für Orte und Veranstaltungen sowie reduzierte Event-Maske: `partials/landing/account/dashboard.php` plus Speicherung in `src/App/ListingCatalog.php`
- Freigaben sowie Änderungs- und Löschanfragen für Orte und Veranstaltungen: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php` - Freigaben sowie Änderungs- und Löschanfragen für Orte und Veranstaltungen: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php`
- Rollenvergabe per Benutzersuche im Community-Admin: `partials/landing/account/community-admin.php`, `src/App/CommunityAccess.php` - Rollenvergabe per Benutzersuche im Community-Admin: `partials/landing/account/community-admin.php`, `src/App/CommunityAccess.php`

View File

@@ -51,6 +51,8 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
- System levels remain separate as the roles `Forum Admin`, `Site Admin`, and `SiteOwner`. - System levels remain separate as the roles `Forum Admin`, `Site Admin`, and `SiteOwner`.
- System-level assignment in `Community Admin` now runs through user search instead of raw user IDs; site admins remain limited by the existing role rules. - System-level assignment in `Community Admin` now runs through user search instead of raw user IDs; site admins remain limited by the existing role rules.
- Categories no longer live under `System`, but in their own member-area section `Categories`; that area only shows newly created categories and merges them into existing database categories through a suggestion field. - Categories no longer live under `System`, but in their own member-area section `Categories`; that area only shows newly created categories and merges them into existing database categories through a suggestion field.
- New categories remain visible there until an authorized user confirms them or merges them into an existing category.
- Community levels can now additionally carry the rights for category handling as well as place and event approvals.
## Technical Frame ## Technical Frame
- custom front controller in `public/index.php` - custom front controller in `public/index.php`

View File

@@ -25,6 +25,7 @@ Updated: 2026-08-10
- calendar export and subscribable feed: `src/App/CalendarSync.php` plus `public/page/calendar/export.php` and `public/page/calendar/feed.php` - calendar export and subscribable feed: `src/App/CalendarSync.php` plus `public/page/calendar/export.php` and `public/page/calendar/feed.php`
- separated `Events` and `Places & Events` UI in member area: `partials/landing/account/dashboard.php` - separated `Events` and `Places & Events` UI in member area: `partials/landing/account/dashboard.php`
- dedicated member-area section `Categories` for new categories and merging through a suggestion field: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php` - dedicated member-area section `Categories` for new categories and merging through a suggestion field: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php`
- community-level rights for categories and place/event approvals: `config/community.php`, `src/App/Community.php`, `src/App/CommunityAccess.php`, `partials/landing/account/community-admin.php`
- pricing, image upload, and place/address input for places and event-style entries plus a reduced own-event form: `partials/landing/account/dashboard.php` with persistence in `src/App/ListingCatalog.php` - pricing, image upload, and place/address input for places and event-style entries plus a reduced own-event form: `partials/landing/account/dashboard.php` with persistence in `src/App/ListingCatalog.php`
- approvals as well as change and deletion requests for places and event-style entries: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php` - approvals as well as change and deletion requests for places and event-style entries: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php`
- role assignment via user search in community admin: `partials/landing/account/community-admin.php`, `src/App/CommunityAccess.php` - role assignment via user search in community admin: `partials/landing/account/community-admin.php`, `src/App/CommunityAccess.php`

View File

@@ -42,6 +42,8 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf:
- In `Profil-Levels` werden zusätzlich Nutzer angezeigt, die in den letzten 30 Tagen mindestens den Rang `Säule der Väter-Community` erreicht haben - In `Profil-Levels` werden zusätzlich Nutzer angezeigt, die in den letzten 30 Tagen mindestens den Rang `Säule der Väter-Community` erreicht haben
- Im Bereich `Community-Admin` läuft die Rollenvergabe jetzt über eine Benutzersuche statt über rohe Benutzer-IDs; `Site-Admins` bleiben dabei auf die vorhandene Rollenlogik begrenzt - Im Bereich `Community-Admin` läuft die Rollenvergabe jetzt über eine Benutzersuche statt über rohe Benutzer-IDs; `Site-Admins` bleiben dabei auf die vorhandene Rollenlogik begrenzt
- Kategorien wurden aus `System` in den eigenen Mitgliederbereichspunkt `Kategorien` verschoben; dort werden nur neue Kategorien angezeigt und per Vorschlagsfeld mit bestehenden Kategorien aus der Datenbank zusammengeführt - Kategorien wurden aus `System` in den eigenen Mitgliederbereichspunkt `Kategorien` verschoben; dort werden nur neue Kategorien angezeigt und per Vorschlagsfeld mit bestehenden Kategorien aus der Datenbank zusammengeführt
- Neue Kategorien bleiben im Bereich `Kategorien` sichtbar, bis sie von einem Berechtigten bestätigt oder zusammengeführt werden
- Community-Level können jetzt zusätzlich die Rechte für Kategorien sowie für die Freigabe von Orten und Veranstaltungen tragen
- neue interne Grundstruktur: `listing_places`, `listings`, `listing_occurrences`, `listing_prices`, `listing_benefits` - neue interne Grundstruktur: `listing_places`, `listings`, `listing_occurrences`, `listing_prices`, `listing_benefits`
- Seiten-Admins haben zusätzlich eine System-Sektion für globale Wartungs- und Diensteschalter - Seiten-Admins haben zusätzlich eine System-Sektion für globale Wartungs- und Diensteschalter
- Im Mitgliederbereich ist `Events` jetzt wieder auf echte eigene Events und Event-Teilnahmen beschränkt - Im Mitgliederbereich ist `Events` jetzt wieder auf echte eigene Events und Event-Teilnahmen beschränkt

View File

@@ -41,8 +41,29 @@ return [
['min' => 150, 'label' => 'Unterstützender Vater'], ['min' => 150, 'label' => 'Unterstützender Vater'],
['min' => 300, 'label' => 'Erfahrener Vater'], ['min' => 300, 'label' => 'Erfahrener Vater'],
['min' => 500, 'label' => 'Mentor-Vater'], ['min' => 500, 'label' => 'Mentor-Vater'],
['min' => 750, 'label' => 'Community-Vater'], [
['min' => 1000, 'label' => 'Säule der Väter-Community'], 'min' => 750,
['min' => 1500, 'label' => 'Vater der Gemeinschaft'], 'label' => 'Community-Vater',
'rights' => [
'can_manage_categories' => true,
'can_review_listings' => true,
],
],
[
'min' => 1000,
'label' => 'Säule der Väter-Community',
'rights' => [
'can_manage_categories' => true,
'can_review_listings' => true,
],
],
[
'min' => 1500,
'label' => 'Vater der Gemeinschaft',
'rights' => [
'can_manage_categories' => true,
'can_review_listings' => true,
],
],
], ],
]; ];

View File

@@ -11,10 +11,12 @@ if (!$userId) {
$communityCfg = require __DIR__ . '/../../../config/community.php'; $communityCfg = require __DIR__ . '/../../../config/community.php';
$access = $pdo ? new \App\CommunityAccess($pdo, $communityCfg) : null; $access = $pdo ? new \App\CommunityAccess($pdo, $communityCfg) : null;
$community = $pdo ? new \App\Community($pdo, $communityCfg) : null;
$migration = $pdo ? new \App\CommunityMigration($pdo) : null; $migration = $pdo ? new \App\CommunityMigration($pdo) : null;
$listingCatalog = $pdo ? new \App\ListingCatalog($pdo) : null; $listingCatalog = $pdo ? new \App\ListingCatalog($pdo) : null;
$communityPoints = $community ? $community->computePoints((int)$userId) : 0.0;
if (!$access || !$access->canModerateForum((int)$userId)) { if (!$access || !$access->canAccessCommunityAdmin((int)$userId, $communityPoints)) {
http_response_code(403); http_response_code(403);
echo '<main class="section"><div class="container"><div class="card dash-card"><h1>Kein Zugriff</h1><p class="muted">Dieser Bereich ist nur für Community-Admins freigegeben.</p></div></div></main>'; echo '<main class="section"><div class="container"><div class="card dash-card"><h1>Kein Zugriff</h1><p class="muted">Dieser Bereich ist nur für Community-Admins freigegeben.</p></div></div></main>';
return; return;
@@ -22,9 +24,10 @@ if (!$access || !$access->canModerateForum((int)$userId)) {
$error = ''; $error = '';
$info = ''; $info = '';
$canModerateForum = $access->canModerateForum((int)$userId);
$canManageApplications = $access->canManageApplications((int)$userId) && $access->supportsApplications(); $canManageApplications = $access->canManageApplications((int)$userId) && $access->supportsApplications();
$canManageRoles = $access->canManageRoles((int)$userId); $canManageRoles = $access->canManageRoles((int)$userId);
$canReviewListings = $listingCatalog !== null && !$access->hasRole((int)$userId, 'owner'); $canReviewListings = $listingCatalog !== null && $access->canReviewListings((int)$userId, $communityPoints);
$roleUserSearchQuery = trim((string)($_GET['role_user_query'] ?? '')); $roleUserSearchQuery = trim((string)($_GET['role_user_query'] ?? ''));
$roleUserSearchResults = ($canManageRoles || $canManageApplications) && $roleUserSearchQuery !== '' $roleUserSearchResults = ($canManageRoles || $canManageApplications) && $roleUserSearchQuery !== ''
? $access->searchUsers($roleUserSearchQuery, 12) ? $access->searchUsers($roleUserSearchQuery, 12)
@@ -194,34 +197,36 @@ $migrationStatus = ($migration && $canManageApplications) ? $migration->status()
</section> </section>
<?php endif; ?> <?php endif; ?>
<section class="forum-board"> <?php if ($canModerateForum): ?>
<div class="forum-board__head"> <section class="forum-board">
<div> <div class="forum-board__head">
<h2>Offene Meldungen</h2> <div>
<p class="muted">Gemeldete Inhalte prüfen und abschließen.</p> <h2>Offene Meldungen</h2>
<p class="muted">Gemeldete Inhalte prüfen und abschließen.</p>
</div>
</div> </div>
</div> <div class="forum-admin-list">
<div class="forum-admin-list"> <?php foreach ($reports as $report): ?>
<?php foreach ($reports as $report): ?> <article class="forum-admin-item">
<article class="forum-admin-item"> <div>
<div> <strong><?= htmlspecialchars((string)$report['target_type'], ENT_QUOTES) ?> #<?= (int)$report['target_id'] ?></strong>
<strong><?= htmlspecialchars((string)$report['target_type'], ENT_QUOTES) ?> #<?= (int)$report['target_id'] ?></strong> <p><?= nl2br(htmlspecialchars((string)$report['reason'], ENT_QUOTES)) ?></p>
<p><?= nl2br(htmlspecialchars((string)$report['reason'], ENT_QUOTES)) ?></p> <p class="muted small">Gemeldet von <?= htmlspecialchars((string)($report['reporter_name'] ?: 'Mitglied'), ENT_QUOTES) ?> am <?= htmlspecialchars((string)$report['created_at'], ENT_QUOTES) ?></p>
<p class="muted small">Gemeldet von <?= htmlspecialchars((string)($report['reporter_name'] ?: 'Mitglied'), ENT_QUOTES) ?> am <?= htmlspecialchars((string)$report['created_at'], ENT_QUOTES) ?></p> </div>
</div> <form method="post" class="forum-admin-item__actions">
<form method="post" class="forum-admin-item__actions"> <input type="hidden" name="action" value="report_resolve">
<input type="hidden" name="action" value="report_resolve"> <input type="hidden" name="report_id" value="<?= (int)$report['id'] ?>">
<input type="hidden" name="report_id" value="<?= (int)$report['id'] ?>"> <textarea name="moderator_note" class="textarea" rows="3" placeholder="Moderationsnotiz"></textarea>
<textarea name="moderator_note" class="textarea" rows="3" placeholder="Moderationsnotiz"></textarea> <button class="btn" type="submit">Als erledigt markieren</button>
<button class="btn" type="submit">Als erledigt markieren</button> </form>
</form> </article>
</article> <?php endforeach; ?>
<?php endforeach; ?> <?php if (!$reports): ?>
<?php if (!$reports): ?> <div class="forum-empty">Keine offenen Meldungen.</div>
<div class="forum-empty">Keine offenen Meldungen.</div> <?php endif; ?>
<?php endif; ?> </div>
</div> </section>
</section> <?php endif; ?>
<?php if ($canManageRoles || $canManageApplications): ?> <?php if ($canManageRoles || $canManageApplications): ?>
<section class="forum-board"> <section class="forum-board">

View File

@@ -147,8 +147,10 @@ if (!empty($canManageProfileLevels)) {
} }
$showEventTypeChoice = !$editing && (($section ?? 'events') === 'places') && $requestedCreateListingKind === null; $showEventTypeChoice = !$editing && (($section ?? 'events') === 'places') && $requestedCreateListingKind === null;
$modalSectionTarget = ($editingListing || ($section ?? '') === 'places') ? 'places' : 'events'; $modalSectionTarget = ($editingListing || ($section ?? '') === 'places') ? 'places' : 'events';
if (!empty($canManageSystemSettings)) { if (!empty($canManageCategories)) {
$sectionLinks['categories'] = 'Kategorien'; $sectionLinks['categories'] = 'Kategorien';
}
if (!empty($canManageSystemSettings)) {
$sectionLinks['system'] = 'System'; $sectionLinks['system'] = 'System';
} }
?> ?>
@@ -951,7 +953,7 @@ if (!empty($canManageSystemSettings)) {
</section> </section>
<?php endif; ?> <?php endif; ?>
<?php if ($section === 'categories' && !empty($canManageSystemSettings)): ?> <?php if ($section === 'categories' && !empty($canManageCategories)): ?>
<section class="account-panel"> <section class="account-panel">
<div class="account-panel__head"> <div class="account-panel__head">
<h2>Kategorien</h2> <h2>Kategorien</h2>
@@ -991,6 +993,11 @@ if (!empty($canManageSystemSettings)) {
<button class="btn ghost" type="submit">Zusammenführen</button> <button class="btn ghost" type="submit">Zusammenführen</button>
</div> </div>
</form> </form>
<form method="post" style="margin-top:12px;">
<input type="hidden" name="action" value="category_confirm">
<input type="hidden" name="source_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
<button class="btn" type="submit">Bestätigen</button>
</form>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View File

@@ -6,6 +6,7 @@ $profileInitial = 'P';
$showAdminLink = false; $showAdminLink = false;
$showSystemLink = false; $showSystemLink = false;
$showProfileLevelsLink = false; $showProfileLevelsLink = false;
$showCategoriesLink = false;
if ($isLoggedIn) { if ($isLoggedIn) {
try { try {
@@ -25,10 +26,13 @@ if ($isLoggedIn) {
$communityCfg = dirname(__DIR__, 2) . '/config/community.php'; $communityCfg = dirname(__DIR__, 2) . '/config/community.php';
if (file_exists($communityCfg)) { if (file_exists($communityCfg)) {
$communityConfig = require $communityCfg; $communityConfig = require $communityCfg;
$community = new \App\Community($pdo, $communityConfig);
$communityAccess = new \App\CommunityAccess($pdo, $communityConfig); $communityAccess = new \App\CommunityAccess($pdo, $communityConfig);
$showAdminLink = $communityAccess->canModerateForum((int)$_SESSION['user_id']); $navCommunityPoints = $community->computePoints((int)$_SESSION['user_id']);
$showAdminLink = $communityAccess->canAccessCommunityAdmin((int)$_SESSION['user_id'], $navCommunityPoints);
$showSystemLink = $communityAccess->canManageApplications((int)$_SESSION['user_id']); $showSystemLink = $communityAccess->canManageApplications((int)$_SESSION['user_id']);
$showProfileLevelsLink = $communityAccess->canManageRoles((int)$_SESSION['user_id']); $showProfileLevelsLink = $communityAccess->canManageRoles((int)$_SESSION['user_id']);
$showCategoriesLink = $communityAccess->canManageCategories((int)$_SESSION['user_id'], $navCommunityPoints);
} }
} }
} catch (\Throwable) { } catch (\Throwable) {
@@ -37,6 +41,7 @@ if ($isLoggedIn) {
$showAdminLink = false; $showAdminLink = false;
$showSystemLink = false; $showSystemLink = false;
$showProfileLevelsLink = false; $showProfileLevelsLink = false;
$showCategoriesLink = false;
} }
} }
?> ?>
@@ -68,6 +73,9 @@ if ($isLoggedIn) {
<a href="/dashboard?section=places" role="menuitem">Orte &amp; Veranstaltungen</a> <a href="/dashboard?section=places" role="menuitem">Orte &amp; Veranstaltungen</a>
<a href="/dashboard?section=community" role="menuitem">Community</a> <a href="/dashboard?section=community" role="menuitem">Community</a>
<a href="/dashboard?section=settings" role="menuitem">Einstellungen</a> <a href="/dashboard?section=settings" role="menuitem">Einstellungen</a>
<?php if ($showCategoriesLink): ?>
<a href="/dashboard?section=categories" role="menuitem">Kategorien</a>
<?php endif; ?>
<?php if ($showProfileLevelsLink): ?> <?php if ($showProfileLevelsLink): ?>
<a href="/dashboard?section=profile-levels" role="menuitem">Profil-Levels</a> <a href="/dashboard?section=profile-levels" role="menuitem">Profil-Levels</a>
<?php endif; ?> <?php endif; ?>
@@ -98,6 +106,9 @@ if ($isLoggedIn) {
<a class="btn ghost" href="/dashboard?section=places">Orte &amp; Veranstaltungen</a> <a class="btn ghost" href="/dashboard?section=places">Orte &amp; Veranstaltungen</a>
<a class="btn ghost" href="/dashboard?section=community">Community</a> <a class="btn ghost" href="/dashboard?section=community">Community</a>
<a class="btn ghost" href="/dashboard?section=settings">Einstellungen</a> <a class="btn ghost" href="/dashboard?section=settings">Einstellungen</a>
<?php if ($showCategoriesLink): ?>
<a class="btn ghost" href="/dashboard?section=categories">Kategorien</a>
<?php endif; ?>
<?php if ($showProfileLevelsLink): ?> <?php if ($showProfileLevelsLink): ?>
<a class="btn ghost" href="/dashboard?section=profile-levels">Profil-Levels</a> <a class="btn ghost" href="/dashboard?section=profile-levels">Profil-Levels</a>
<?php endif; ?> <?php endif; ?>

View File

@@ -180,9 +180,13 @@ final class AccountPages
$section = (string)($_GET['section'] ?? 'profile'); $section = (string)($_GET['section'] ?? 'profile');
$canManageSystemSettings = $communityAccess ? $communityAccess->canManageApplications($userId) : false; $canManageSystemSettings = $communityAccess ? $communityAccess->canManageApplications($userId) : false;
$canManageProfileLevels = $communityAccess ? $communityAccess->canManageRoles($userId) : false; $canManageProfileLevels = $communityAccess ? $communityAccess->canManageRoles($userId) : false;
$communityPointsForAccess = $community ? $community->computePoints($userId) : 0.0;
$canManageCategories = $communityAccess ? $communityAccess->canManageCategories($userId, $communityPointsForAccess) : false;
$allowedSections = ['profile', 'children', 'events', 'places', 'community', 'settings']; $allowedSections = ['profile', 'children', 'events', 'places', 'community', 'settings'];
if ($canManageSystemSettings) { if ($canManageSystemSettings) {
$allowedSections[] = 'system'; $allowedSections[] = 'system';
}
if ($canManageCategories) {
$allowedSections[] = 'categories'; $allowedSections[] = 'categories';
} }
if ($canManageProfileLevels) { if ($canManageProfileLevels) {
@@ -326,7 +330,7 @@ final class AccountPages
], $userId); ], $userId);
$info = 'System-Einstellungen gespeichert.'; $info = 'System-Einstellungen gespeichert.';
} elseif ($action === 'category_merge') { } elseif ($action === 'category_merge') {
if (!$canManageSystemSettings || !$listingCatalog) { if (!$canManageCategories || !$listingCatalog) {
throw new \RuntimeException('Keine Berechtigung für die Kategorien-Verwaltung.'); throw new \RuntimeException('Keine Berechtigung für die Kategorien-Verwaltung.');
} }
$targetCategorySlug = $listingCatalog->resolveExistingCategorySlug((string)($_POST['target_category_input'] ?? '')); $targetCategorySlug = $listingCatalog->resolveExistingCategorySlug((string)($_POST['target_category_input'] ?? ''));
@@ -338,6 +342,12 @@ final class AccountPages
$targetCategorySlug $targetCategorySlug
); );
$info = 'Kategorie zusammengeführt.'; $info = 'Kategorie zusammengeführt.';
} elseif ($action === 'category_confirm') {
if (!$canManageCategories || !$listingCatalog) {
throw new \RuntimeException('Keine Berechtigung für die Kategorien-Verwaltung.');
}
$listingCatalog->confirmCategory((string)($_POST['source_category_slug'] ?? ''));
$info = 'Kategorie bestätigt.';
} elseif ($action === 'child_add' || $action === 'child_update') { } elseif ($action === 'child_add' || $action === 'child_update') {
$crypto = self::requireCrypto($crypto, 'Kinder'); $crypto = self::requireCrypto($crypto, 'Kinder');
$childId = (int)($_POST['child_id'] ?? 0); $childId = (int)($_POST['child_id'] ?? 0);
@@ -857,7 +867,7 @@ final class AccountPages
$restoreEventDraftActive = $section === 'events' && ((string)($_GET['restore_event_draft'] ?? '')) === '1' && self::hasStoredEventDraft(); $restoreEventDraftActive = $section === 'events' && ((string)($_GET['restore_event_draft'] ?? '')) === '1' && self::hasStoredEventDraft();
$restoredEventDraft = $restoreEventDraftActive ? self::getStoredEventDraft() : null; $restoredEventDraft = $restoreEventDraftActive ? self::getStoredEventDraft() : null;
$listingCategories = $listingCatalog ? $listingCatalog->listCategories(['general', 'place', 'food', 'event', 'family']) : []; $listingCategories = $listingCatalog ? $listingCatalog->listCategories(['general', 'place', 'food', 'event', 'family']) : [];
$categoryReviewItems = $listingCatalog && $canManageSystemSettings ? array_values(array_filter( $categoryReviewItems = $listingCatalog && $canManageCategories ? array_values(array_filter(
$listingCatalog->listCategoryReviewItems(), $listingCatalog->listCategoryReviewItems(),
static fn(array $item): bool => !empty($item['needs_review']) static fn(array $item): bool => !empty($item['needs_review'])
)) : []; )) : [];
@@ -940,7 +950,7 @@ final class AccountPages
} }
} }
$communityPoints = $community ? $community->computePoints($userId) : 0.0; $communityPoints = $communityPointsForAccess;
$communityLevel = $community ? $community->membershipLevel($communityPoints) : ['label' => '', 'icon' => '']; $communityLevel = $community ? $community->membershipLevel($communityPoints) : ['label' => '', 'icon' => ''];
$communityLevelDefinitions = $community ? $community->listMembershipLevels() : []; $communityLevelDefinitions = $community ? $community->listMembershipLevels() : [];
$communityLevelRightDefinitions = Community::membershipRightDefinitions(); $communityLevelRightDefinitions = Community::membershipRightDefinitions();
@@ -990,6 +1000,7 @@ final class AccountPages
'Themen und Antworten moderieren', 'Themen und Antworten moderieren',
'Community-Sperren setzen und aufheben', 'Community-Sperren setzen und aufheben',
'Meldungen bearbeiten', 'Meldungen bearbeiten',
'Orte und Veranstaltungen freigeben',
], ],
], ],
[ [
@@ -998,6 +1009,7 @@ final class AccountPages
'rights' => [ 'rights' => [
'Beinhaltet alle Rechte von Forum-Admin', 'Beinhaltet alle Rechte von Forum-Admin',
'Community-Admin-Bewerbungen bearbeiten', 'Community-Admin-Bewerbungen bearbeiten',
'Kategorien bestätigen und zusammenführen',
'System-Einstellungen verwalten', 'System-Einstellungen verwalten',
], ],
], ],
@@ -1061,6 +1073,7 @@ final class AccountPages
'communityCanApply', 'communityCanApply',
'communityRestrictions', 'communityRestrictions',
'systemLevelDefinitions', 'systemLevelDefinitions',
'canManageCategories',
'canManageSystemSettings', 'canManageSystemSettings',
'canManageProfileLevels', 'canManageProfileLevels',
'systemSettingsValues', 'systemSettingsValues',

View File

@@ -657,6 +657,12 @@ final class Community
'can_apply_for_forum_admin' => [ 'can_apply_for_forum_admin' => [
'label' => 'Bewerbung als Forum-Admin', 'label' => 'Bewerbung als Forum-Admin',
], ],
'can_manage_categories' => [
'label' => 'Kategorien bestätigen und zusammenführen',
],
'can_review_listings' => [
'label' => 'Orte und Veranstaltungen freigeben',
],
]; ];
} }

View File

@@ -75,6 +75,35 @@ final class CommunityAccess
return $this->hasRole($userId, 'owner'); return $this->hasRole($userId, 'owner');
} }
public function canManageCategories(int $userId, ?float $points = null): bool
{
if ($this->hasRole($userId, 'owner') || $this->hasRole($userId, 'site_admin')) {
return true;
}
return $this->resolveLevelRight($points ?? $this->resolveUserPoints($userId), 'can_manage_categories');
}
public function canReviewListings(int $userId, ?float $points = null): bool
{
if ($this->hasRole($userId, 'owner')) {
return false;
}
if ($this->hasRole($userId, 'site_admin') || $this->hasRole($userId, 'forum_admin')) {
return true;
}
return $this->resolveLevelRight($points ?? $this->resolveUserPoints($userId), 'can_review_listings');
}
public function canAccessCommunityAdmin(int $userId, ?float $points = null): bool
{
return $this->canModerateForum($userId)
|| $this->canManageApplications($userId)
|| $this->canManageRoles($userId)
|| $this->canReviewListings($userId, $points);
}
public function getRestrictionState(int $userId): array public function getRestrictionState(int $userId): array
{ {
$state = [ $state = [
@@ -574,4 +603,14 @@ final class CommunityAccess
$level = $community->membershipLevelMeta($points); $level = $community->membershipLevelMeta($points);
return !empty($level['rights'][$rightKey]); return !empty($level['rights'][$rightKey]);
} }
private function resolveUserPoints(int $userId): float
{
if ($userId <= 0) {
return 0.0;
}
$community = new Community($this->pdo, $this->communityConfig);
return $community->computePoints($userId);
}
} }

View File

@@ -368,6 +368,35 @@ final class ListingCatalog
} }
} }
public function confirmCategory(string $sourceSlug): void
{
$this->ensureSchema();
$sourceSlug = trim($sourceSlug);
if ($sourceSlug === '') {
throw new \RuntimeException('Kategorie nicht gefunden.');
}
$lookup = $this->pdo->prepare('SELECT id, sort_order FROM listing_categories WHERE slug = :slug LIMIT 1');
$lookup->execute(['slug' => $sourceSlug]);
$source = $lookup->fetch(\PDO::FETCH_ASSOC);
if (!$source) {
throw new \RuntimeException('Kategorie nicht gefunden.');
}
if ((int)($source['sort_order'] ?? 0) < 900) {
return;
}
$maxStmt = $this->pdo->query('SELECT COALESCE(MAX(sort_order), 0) FROM listing_categories WHERE sort_order < 900');
$maxSortOrder = (int)($maxStmt?->fetchColumn() ?: 0);
$newSortOrder = max(1, $maxSortOrder + 1);
$update = $this->pdo->prepare('UPDATE listing_categories SET sort_order = :sortOrder, updated_at = NOW() WHERE id = :id');
$update->execute([
'sortOrder' => $newSortOrder,
'id' => (int)$source['id'],
]);
}
public function resolveExistingCategorySlug(string $input): ?string public function resolveExistingCategorySlug(string $input): ?string
{ {
$this->ensureSchema(); $this->ensureSchema();