diff --git a/Internal/de/PROJECT_CONTEXT.md b/Internal/de/PROJECT_CONTEXT.md index 71dfedc..42aa391 100644 --- a/Internal/de/PROJECT_CONTEXT.md +++ b/Internal/de/PROJECT_CONTEXT.md @@ -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`. - 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. +- 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 - eigener Front-Controller in `public/index.php` diff --git a/Internal/de/PROJECT_STRUCTURE.md b/Internal/de/PROJECT_STRUCTURE.md index df35623..25e8dec 100644 --- a/Internal/de/PROJECT_STRUCTURE.md +++ b/Internal/de/PROJECT_STRUCTURE.md @@ -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` - 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` +- 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` - 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` diff --git a/Internal/en/PROJECT_CONTEXT.md b/Internal/en/PROJECT_CONTEXT.md index 3c84cb8..5f5266c 100644 --- a/Internal/en/PROJECT_CONTEXT.md +++ b/Internal/en/PROJECT_CONTEXT.md @@ -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-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. +- 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 - custom front controller in `public/index.php` diff --git a/Internal/en/PROJECT_STRUCTURE.md b/Internal/en/PROJECT_STRUCTURE.md index e2504df..68593e2 100644 --- a/Internal/en/PROJECT_STRUCTURE.md +++ b/Internal/en/PROJECT_STRUCTURE.md @@ -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` - 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` +- 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` - 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` diff --git a/README.md b/README.md index 263b737..dba2de6 100644 --- a/README.md +++ b/README.md @@ -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 - 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 +- 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` - 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 diff --git a/config/community.php b/config/community.php index f3b73e5..89d5d62 100755 --- a/config/community.php +++ b/config/community.php @@ -41,8 +41,29 @@ return [ ['min' => 150, 'label' => 'Unterstützender Vater'], ['min' => 300, 'label' => 'Erfahrener Vater'], ['min' => 500, 'label' => 'Mentor-Vater'], - ['min' => 750, 'label' => 'Community-Vater'], - ['min' => 1000, 'label' => 'Säule der Väter-Community'], - ['min' => 1500, 'label' => 'Vater der Gemeinschaft'], + [ + 'min' => 750, + '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, + ], + ], ], ]; diff --git a/partials/landing/account/community-admin.php b/partials/landing/account/community-admin.php index ec7cae9..d325957 100644 --- a/partials/landing/account/community-admin.php +++ b/partials/landing/account/community-admin.php @@ -11,10 +11,12 @@ if (!$userId) { $communityCfg = require __DIR__ . '/../../../config/community.php'; $access = $pdo ? new \App\CommunityAccess($pdo, $communityCfg) : null; +$community = $pdo ? new \App\Community($pdo, $communityCfg) : null; $migration = $pdo ? new \App\CommunityMigration($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); echo '

Kein Zugriff

Dieser Bereich ist nur für Community-Admins freigegeben.

'; return; @@ -22,9 +24,10 @@ if (!$access || !$access->canModerateForum((int)$userId)) { $error = ''; $info = ''; +$canModerateForum = $access->canModerateForum((int)$userId); $canManageApplications = $access->canManageApplications((int)$userId) && $access->supportsApplications(); $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'] ?? '')); $roleUserSearchResults = ($canManageRoles || $canManageApplications) && $roleUserSearchQuery !== '' ? $access->searchUsers($roleUserSearchQuery, 12) @@ -194,34 +197,36 @@ $migrationStatus = ($migration && $canManageApplications) ? $migration->status() -
-
-
-

Offene Meldungen

-

Gemeldete Inhalte prüfen und abschließen.

+ +
+
+
+

Offene Meldungen

+

Gemeldete Inhalte prüfen und abschließen.

+
-
-
- -
-
- # -

-

Gemeldet von am

-
-
- - - - -
-
- - -
Keine offenen Meldungen.
- -
-
+
+ +
+
+ # +

+

Gemeldet von am

+
+
+ + + + +
+
+ + +
Keine offenen Meldungen.
+ +
+ +
diff --git a/partials/landing/account/dashboard.php b/partials/landing/account/dashboard.php index 6fed6a0..916d378 100755 --- a/partials/landing/account/dashboard.php +++ b/partials/landing/account/dashboard.php @@ -147,8 +147,10 @@ if (!empty($canManageProfileLevels)) { } $showEventTypeChoice = !$editing && (($section ?? 'events') === 'places') && $requestedCreateListingKind === null; $modalSectionTarget = ($editingListing || ($section ?? '') === 'places') ? 'places' : 'events'; -if (!empty($canManageSystemSettings)) { +if (!empty($canManageCategories)) { $sectionLinks['categories'] = 'Kategorien'; +} +if (!empty($canManageSystemSettings)) { $sectionLinks['system'] = 'System'; } ?> @@ -951,7 +953,7 @@ if (!empty($canManageSystemSettings)) {
- +

Kategorien

@@ -991,6 +993,11 @@ if (!empty($canManageSystemSettings)) {
+
+ + + +
diff --git a/partials/structure/nav.php b/partials/structure/nav.php index 3f9fa94..7766e36 100755 --- a/partials/structure/nav.php +++ b/partials/structure/nav.php @@ -6,6 +6,7 @@ $profileInitial = 'P'; $showAdminLink = false; $showSystemLink = false; $showProfileLevelsLink = false; +$showCategoriesLink = false; if ($isLoggedIn) { try { @@ -25,10 +26,13 @@ if ($isLoggedIn) { $communityCfg = dirname(__DIR__, 2) . '/config/community.php'; if (file_exists($communityCfg)) { $communityConfig = require $communityCfg; + $community = new \App\Community($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']); $showProfileLevelsLink = $communityAccess->canManageRoles((int)$_SESSION['user_id']); + $showCategoriesLink = $communityAccess->canManageCategories((int)$_SESSION['user_id'], $navCommunityPoints); } } } catch (\Throwable) { @@ -37,6 +41,7 @@ if ($isLoggedIn) { $showAdminLink = false; $showSystemLink = false; $showProfileLevelsLink = false; + $showCategoriesLink = false; } } ?> @@ -68,6 +73,9 @@ if ($isLoggedIn) { Orte & Veranstaltungen Community Einstellungen + + Kategorien + Profil-Levels @@ -98,6 +106,9 @@ if ($isLoggedIn) { Orte & Veranstaltungen Community Einstellungen + + Kategorien + Profil-Levels diff --git a/src/App/AccountPages.php b/src/App/AccountPages.php index bea2670..10adcce 100755 --- a/src/App/AccountPages.php +++ b/src/App/AccountPages.php @@ -180,9 +180,13 @@ final class AccountPages $section = (string)($_GET['section'] ?? 'profile'); $canManageSystemSettings = $communityAccess ? $communityAccess->canManageApplications($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']; if ($canManageSystemSettings) { $allowedSections[] = 'system'; + } + if ($canManageCategories) { $allowedSections[] = 'categories'; } if ($canManageProfileLevels) { @@ -326,7 +330,7 @@ final class AccountPages ], $userId); $info = 'System-Einstellungen gespeichert.'; } elseif ($action === 'category_merge') { - if (!$canManageSystemSettings || !$listingCatalog) { + if (!$canManageCategories || !$listingCatalog) { throw new \RuntimeException('Keine Berechtigung für die Kategorien-Verwaltung.'); } $targetCategorySlug = $listingCatalog->resolveExistingCategorySlug((string)($_POST['target_category_input'] ?? '')); @@ -338,6 +342,12 @@ final class AccountPages $targetCategorySlug ); $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') { $crypto = self::requireCrypto($crypto, 'Kinder'); $childId = (int)($_POST['child_id'] ?? 0); @@ -857,7 +867,7 @@ final class AccountPages $restoreEventDraftActive = $section === 'events' && ((string)($_GET['restore_event_draft'] ?? '')) === '1' && self::hasStoredEventDraft(); $restoredEventDraft = $restoreEventDraftActive ? self::getStoredEventDraft() : null; $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(), 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' => '']; $communityLevelDefinitions = $community ? $community->listMembershipLevels() : []; $communityLevelRightDefinitions = Community::membershipRightDefinitions(); @@ -990,6 +1000,7 @@ final class AccountPages 'Themen und Antworten moderieren', 'Community-Sperren setzen und aufheben', 'Meldungen bearbeiten', + 'Orte und Veranstaltungen freigeben', ], ], [ @@ -998,6 +1009,7 @@ final class AccountPages 'rights' => [ 'Beinhaltet alle Rechte von Forum-Admin', 'Community-Admin-Bewerbungen bearbeiten', + 'Kategorien bestätigen und zusammenführen', 'System-Einstellungen verwalten', ], ], @@ -1061,6 +1073,7 @@ final class AccountPages 'communityCanApply', 'communityRestrictions', 'systemLevelDefinitions', + 'canManageCategories', 'canManageSystemSettings', 'canManageProfileLevels', 'systemSettingsValues', diff --git a/src/App/Community.php b/src/App/Community.php index e22967d..c5858c2 100755 --- a/src/App/Community.php +++ b/src/App/Community.php @@ -657,6 +657,12 @@ final class Community 'can_apply_for_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', + ], ]; } diff --git a/src/App/CommunityAccess.php b/src/App/CommunityAccess.php index b67366c..0260774 100644 --- a/src/App/CommunityAccess.php +++ b/src/App/CommunityAccess.php @@ -75,6 +75,35 @@ final class CommunityAccess 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 { $state = [ @@ -574,4 +603,14 @@ final class CommunityAccess $level = $community->membershipLevelMeta($points); 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); + } } diff --git a/src/App/ListingCatalog.php b/src/App/ListingCatalog.php index 8080135..dc53548 100644 --- a/src/App/ListingCatalog.php +++ b/src/App/ListingCatalog.php @@ -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 { $this->ensureSchema();