diff --git a/Internal/de/PROJECT_CONTEXT.md b/Internal/de/PROJECT_CONTEXT.md index 88761d2..ac7d8de 100644 --- a/Internal/de/PROJECT_CONTEXT.md +++ b/Internal/de/PROJECT_CONTEXT.md @@ -46,7 +46,9 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l - Eigene Events unterstützen optionalen Beschreibungstext, Ja/Nein-Angabe `Mit Kindern`, optionale Platzzahl, optionales Bild sowie eine Location aus Datenbank oder benutzerdefinierte Adresse. - `Profil-Levels` ist ein Owner-Bereich für Community-Level und die Übersicht der System-Level. - Community-Level sind jetzt über `system_settings.community_levels_json` editierbar und enthalten aktuell die Rechte `Hilfreiche Antworten hervorheben` und `Bewerbung als Forum-Admin`. +- In `Profil-Levels` kann der Owner zusätzlich Benutzer suchen und Community-Punkte manuell mit Begründung erhöhen. - 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. ## Technischer Rahmen - eigener Front-Controller in `public/index.php` diff --git a/Internal/de/PROJECT_STRUCTURE.md b/Internal/de/PROJECT_STRUCTURE.md index 9ec49cf..103ed7d 100644 --- a/Internal/de/PROJECT_STRUCTURE.md +++ b/Internal/de/PROJECT_STRUCTURE.md @@ -20,12 +20,13 @@ Stand: 2026-08-10 - Event-Quellen aus Datenbank plus Speicherung der Event-Location-Herkunft: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `Internal/db/schema.sql` - Consent- und rechtliche Texte: `public/page/datenschutz.php` - System-Einstellungen: `src/App/SystemSettings.php` -- Profil-Levels für Owner: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/Community.php`, `src/App/CommunityAccess.php`, `src/App/SystemSettings.php` +- Profil-Levels für Owner inklusive Benutzersuche und manueller Community-Punktevergabe: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/Community.php`, `src/App/CommunityAccess.php`, `src/App/SystemSettings.php` - neue Listing-/Ort-Basis: `src/App/ListingCatalog.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` - 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` - Legacy-Eigen-Events wurden erweitert in `src/App/AccountPages.php` und `Internal/db/schema.sql` um Kategorie- und Bildfelder - Kategorien-Prüfung und Zusammenführung für Seiten-Admins liegen ebenfalls in `partials/landing/account/dashboard.php` mit Logik in `src/App/ListingCatalog.php` diff --git a/Internal/en/PROJECT_CONTEXT.md b/Internal/en/PROJECT_CONTEXT.md index d7cea63..fceb070 100644 --- a/Internal/en/PROJECT_CONTEXT.md +++ b/Internal/en/PROJECT_CONTEXT.md @@ -46,7 +46,9 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events - Own events support an optional description, a clear `with children` yes/no field, optional capacity, an optional image, and either a database-backed location or a custom address. - `Profile Levels` is an owner-only area for community levels and the overview of system levels. - Community levels are now editable through `system_settings.community_levels_json` and currently carry the rights `highlight helpful replies` and `apply for forum admin`. +- Inside `Profile Levels`, the owner can additionally search users and manually increase community points with a required reason. - 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. ## Technical Frame - custom front controller in `public/index.php` diff --git a/Internal/en/PROJECT_STRUCTURE.md b/Internal/en/PROJECT_STRUCTURE.md index 84a23fe..c6c9fa9 100644 --- a/Internal/en/PROJECT_STRUCTURE.md +++ b/Internal/en/PROJECT_STRUCTURE.md @@ -20,12 +20,13 @@ Updated: 2026-08-10 - database-backed event location sources plus persisted event location origin: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `Internal/db/schema.sql` - consent and legal texts: `public/page/datenschutz.php` - system settings: `src/App/SystemSettings.php` -- owner profile-level management: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/Community.php`, `src/App/CommunityAccess.php`, `src/App/SystemSettings.php` +- owner profile-level management including user search and manual community-point increases: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/Community.php`, `src/App/CommunityAccess.php`, `src/App/SystemSettings.php` - new listing/place base: `src/App/ListingCatalog.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` - 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` - legacy own events were extended in `src/App/AccountPages.php` and `Internal/db/schema.sql` with category and image fields ## Documentation Rule diff --git a/README.md b/README.md index 1b67563..3a53495 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf: - Hauptnavigation aktuell: `Home`, `Event Suche`, `Community` - Profil-Menü aktuell: `Profil`, `Kinder`, `Events`, `Orte & Veranstaltungen`, `Community`, `Einstellungen`, `Abmelden` - Für `SiteOwner` gibt es zusätzlich den Mitgliederbereichspunkt `Profil-Levels` +- In `Profil-Levels` kann der `SiteOwner` Community-Level pflegen, Benutzer suchen und Community-Punkte manuell mit Begründung erhöhen +- 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 - 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/partials/landing/account/community-admin.php b/partials/landing/account/community-admin.php index 7c30868..ec7cae9 100644 --- a/partials/landing/account/community-admin.php +++ b/partials/landing/account/community-admin.php @@ -25,6 +25,10 @@ $info = ''; $canManageApplications = $access->canManageApplications((int)$userId) && $access->supportsApplications(); $canManageRoles = $access->canManageRoles((int)$userId); $canReviewListings = $listingCatalog !== null && !$access->hasRole((int)$userId, 'owner'); +$roleUserSearchQuery = trim((string)($_GET['role_user_query'] ?? '')); +$roleUserSearchResults = ($canManageRoles || $canManageApplications) && $roleUserSearchQuery !== '' + ? $access->searchUsers($roleUserSearchQuery, 12) + : []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = (string)($_POST['action'] ?? ''); @@ -219,33 +223,64 @@ $migrationStatus = ($migration && $canManageApplications) ? $migration->status() - +

Rollen verwalten

-

Community-Rollen vergeben und entziehen.

-
- +
- - -
-
- - + +
- +
+ +
+ + getUserRoles($candidateUserId); + $assignableRoles = $canManageRoles ? ['forum_admin', 'site_admin', 'owner'] : ['forum_admin']; + ?> +
+ +
+ ID: · +
+ +
Aktuelle Rollen:
+ +
+ + +
+ + + + +
+ +
+ + + + +
+ + +
+
+ +
+ +

Keine passenden Benutzer gefunden.

+
diff --git a/partials/landing/account/dashboard.php b/partials/landing/account/dashboard.php index 3174203..202d04a 100755 --- a/partials/landing/account/dashboard.php +++ b/partials/landing/account/dashboard.php @@ -340,6 +340,64 @@ if (!empty($canManageSystemSettings)) {

Profil-Levels

+
+ Benutzer suchen +
+ +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ ID: · + · + Punkte: · + Level: +
+ +
System-Level:
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ +
+ +

Keine passenden Benutzer gefunden.

+ +
+
Community-Levels
diff --git a/src/App/AccountPages.php b/src/App/AccountPages.php index e199bb8..86fdeca 100755 --- a/src/App/AccountPages.php +++ b/src/App/AccountPages.php @@ -707,6 +707,31 @@ final class AccountPages } $community->saveMembershipLevels($levels, $userId); $info = 'Community-Level gelöscht.'; + } elseif ($action === 'community_points_adjust') { + if (!$canManageProfileLevels || !$community) { + throw new \RuntimeException('Keine Berechtigung für Community-Punkte.'); + } + + $targetUserId = (int)($_POST['target_user_id'] ?? 0); + $amountRaw = trim((string)($_POST['points_amount'] ?? '')); + $reason = trim((string)($_POST['points_reason'] ?? '')); + if ($targetUserId <= 0) { + throw new \RuntimeException('Bitte wähle einen Benutzer aus.'); + } + if ($amountRaw === '' || !is_numeric($amountRaw)) { + throw new \RuntimeException('Bitte gib eine gültige Punktzahl an.'); + } + + $amount = (float)$amountRaw; + if ($amount <= 0) { + throw new \RuntimeException('Es können hier nur zusätzliche Punkte vergeben werden.'); + } + if ($reason === '') { + throw new \RuntimeException('Bitte gib eine Begründung für die Punktevergabe an.'); + } + + $community->adjustPoints($targetUserId, $amount, $reason, $userId); + $info = 'Community-Punkte wurden erhöht.'; } } catch (\Throwable $e) { $error = $e->getMessage(); @@ -913,6 +938,21 @@ final class AccountPages $communityLevelRightDefinitions = Community::membershipRightDefinitions(); $communityRoles = $communityAccess ? $communityAccess->getUserRoles($userId) : []; $systemRoleAssignments = $communityAccess && $canManageProfileLevels ? $communityAccess->listRoleAssignments() : []; + $profileLevelUserSearchQuery = ''; + $profileLevelUserSearchResults = []; + if ($communityAccess && $community && $canManageProfileLevels) { + $profileLevelUserSearchQuery = trim((string)($_GET['profile_level_user_query'] ?? '')); + if ($profileLevelUserSearchQuery !== '') { + foreach ($communityAccess->searchUsers($profileLevelUserSearchQuery, 12) as $searchRow) { + $targetSearchUserId = (int)($searchRow['id'] ?? 0); + $targetPoints = $community->computePoints($targetSearchUserId); + $searchRow['community_points'] = $targetPoints; + $searchRow['community_level'] = $community->membershipLevel($targetPoints); + $searchRow['roles'] = $communityAccess->getUserRoles($targetSearchUserId); + $profileLevelUserSearchResults[] = $searchRow; + } + } + } $communityApplication = $communityAccess ? $communityAccess->getLatestApplication($userId) : null; $communityCanApply = $communityAccess ? $communityAccess->canApplyForForumAdmin($userId, $communityPoints) : false; $communityRestrictions = $communityAccess ? $communityAccess->getRestrictionState($userId) : [ @@ -991,6 +1031,8 @@ final class AccountPages 'communityLevelRightDefinitions', 'communityRoles', 'systemRoleAssignments', + 'profileLevelUserSearchQuery', + 'profileLevelUserSearchResults', 'communityApplication', 'communityCanApply', 'communityRestrictions', diff --git a/src/App/Community.php b/src/App/Community.php index 962e072..8a9ad06 100755 --- a/src/App/Community.php +++ b/src/App/Community.php @@ -417,6 +417,36 @@ final class Community return $amount; } + public function adjustPoints(int $userId, float $amount, string $reason = '', ?int $actingUserId = null): float + { + if ($userId <= 0) { + throw new \RuntimeException('Benutzer nicht gefunden.'); + } + if ($amount == 0.0) { + throw new \RuntimeException('Bitte gib eine Punkteänderung ungleich 0 an.'); + } + + $meta = [ + 'reason' => trim($reason), + ]; + if ($actingUserId !== null && $actingUserId > 0) { + $meta['acting_user_id'] = $actingUserId; + } + + $stmt = $this->pdo->prepare('INSERT INTO user_points (user_id, action, amount, meta) VALUES (:uid, :action, :amount, :meta)'); + $stmt->execute([ + ':uid' => $userId, + ':action' => 'manual.adjustment', + ':amount' => $amount, + ':meta' => json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ]); + + $stmt = $this->pdo->prepare('INSERT INTO user_points_totals (user_id, total) VALUES (:uid, :amt) ON DUPLICATE KEY UPDATE total = total + VALUES(total)'); + $stmt->execute([':uid' => $userId, ':amt' => $amount]); + + return $amount; + } + public function membershipLevel(float $points): array { $level = $this->membershipLevelMeta($points); diff --git a/src/App/CommunityAccess.php b/src/App/CommunityAccess.php index a319d25..99d5a35 100644 --- a/src/App/CommunityAccess.php +++ b/src/App/CommunityAccess.php @@ -290,6 +290,53 @@ final class CommunityAccess return $this->emailStore()->decryptRowEmails($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [], 'user_id'); } + public function searchUsers(string $query, int $limit = 12): array + { + $query = trim($query); + if ($query === '') { + return []; + } + + $limit = max(1, min(50, $limit)); + $tokens = array_values(array_filter(preg_split('/\s+/u', mb_strtolower($query)) ?: [], static fn(string $token): bool => $token !== '')); + if ($tokens === []) { + return []; + } + + $conditions = []; + $params = []; + foreach ($tokens as $index => $token) { + $displayKey = ':display_' . $index; + $nameKey = ':name_' . $index; + $emailKey = ':email_' . $index; + $conditions[] = '(LOWER(COALESCE(up.display_name, "")) LIKE ' . $displayKey . ' OR LOWER(CONCAT_WS(" ", COALESCE(up.first_name, ""), COALESCE(up.last_name, ""))) LIKE ' . $nameKey . ' OR LOWER(COALESCE(u.email, "")) LIKE ' . $emailKey . ')'; + $needle = '%' . $token . '%'; + $params[$displayKey] = $needle; + $params[$nameKey] = $needle; + $params[$emailKey] = $needle; + } + + $sql = ' + SELECT u.id, u.email, u.status, up.display_name, up.first_name, up.last_name + FROM users u + LEFT JOIN user_profiles up ON up.user_id = u.id + WHERE ' . implode(' AND ', $conditions) . ' + ORDER BY + CASE + WHEN LOWER(COALESCE(up.display_name, "")) = :exactQuery THEN 0 + WHEN LOWER(COALESCE(u.email, "")) = :exactQuery THEN 1 + ELSE 2 + END, + COALESCE(up.display_name, ""), + u.id DESC + LIMIT ' . $limit; + $params[':exactQuery'] = mb_strtolower($query); + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + return $this->emailStore()->decryptRowEmails($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [], 'id'); + } + public function setRestriction(int $actingUserId, int $targetUserId, string $type, string $reason): void { if (!$this->canModerateForum($actingUserId)) {