From 0f57f90568783563aabd201e45b7e0d49b13b68f Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Mon, 17 Aug 2026 00:34:41 +0200 Subject: [PATCH] sdasd --- Internal/de/PROJECT_CONTEXT.md | 1 + Internal/de/PROJECT_STRUCTURE.md | 2 +- Internal/en/PROJECT_CONTEXT.md | 1 + Internal/en/PROJECT_STRUCTURE.md | 2 +- README.md | 1 + partials/landing/account/dashboard.php | 24 +++++++ src/App/AccountPages.php | 16 +++++ src/App/Community.php | 93 ++++++++++++++++++++++++++ src/App/CommunityAccess.php | 7 +- 9 files changed, 142 insertions(+), 5 deletions(-) diff --git a/Internal/de/PROJECT_CONTEXT.md b/Internal/de/PROJECT_CONTEXT.md index ac7d8de..02dc2d3 100644 --- a/Internal/de/PROJECT_CONTEXT.md +++ b/Internal/de/PROJECT_CONTEXT.md @@ -47,6 +47,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l - `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. +- In `Profil-Levels` gibt es zusätzlich eine Liste von Nutzern, die in den letzten 30 Tagen mindestens den Rang `Säule der Väter-Community` erreicht haben. - 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. diff --git a/Internal/de/PROJECT_STRUCTURE.md b/Internal/de/PROJECT_STRUCTURE.md index 103ed7d..ebced59 100644 --- a/Internal/de/PROJECT_STRUCTURE.md +++ b/Internal/de/PROJECT_STRUCTURE.md @@ -20,7 +20,7 @@ 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 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` +- Profil-Levels für Owner inklusive Benutzersuche, manueller Community-Punktevergabe und Liste kürzlich erreichter hoher Ränge: `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` diff --git a/Internal/en/PROJECT_CONTEXT.md b/Internal/en/PROJECT_CONTEXT.md index fceb070..582bad9 100644 --- a/Internal/en/PROJECT_CONTEXT.md +++ b/Internal/en/PROJECT_CONTEXT.md @@ -47,6 +47,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events - `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. +- `Profile Levels` also includes a list of users who reached at least the rank `Pillar of the Fathers Community` within the last 30 days. - 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. diff --git a/Internal/en/PROJECT_STRUCTURE.md b/Internal/en/PROJECT_STRUCTURE.md index c6c9fa9..f30c3e7 100644 --- a/Internal/en/PROJECT_STRUCTURE.md +++ b/Internal/en/PROJECT_STRUCTURE.md @@ -20,7 +20,7 @@ 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 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` +- owner profile-level management including user search, manual community-point increases, and recently reached high-rank lists: `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` diff --git a/README.md b/README.md index 3a53495..c3444d8 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf: - 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 +- 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 - 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 diff --git a/partials/landing/account/dashboard.php b/partials/landing/account/dashboard.php index 202d04a..9bfb02c 100755 --- a/partials/landing/account/dashboard.php +++ b/partials/landing/account/dashboard.php @@ -340,6 +340,30 @@ if (!empty($canManageSystemSettings)) {

Profil-Levels

+
+ Kürzlich erreicht + +
    + + +
  • + +
    + · + Punkte · + +
    +
  • + +
+ +

Keine neuen Aufstiege vorhanden.

+ +
+
Benutzer suchen
diff --git a/src/App/AccountPages.php b/src/App/AccountPages.php index 86fdeca..6a124ad 100755 --- a/src/App/AccountPages.php +++ b/src/App/AccountPages.php @@ -938,9 +938,23 @@ final class AccountPages $communityLevelRightDefinitions = Community::membershipRightDefinitions(); $communityRoles = $communityAccess ? $communityAccess->getUserRoles($userId) : []; $systemRoleAssignments = $communityAccess && $canManageProfileLevels ? $communityAccess->listRoleAssignments() : []; + $recentHighLevelUsers = []; + $recentHighLevelThresholdLabel = ''; $profileLevelUserSearchQuery = ''; $profileLevelUserSearchResults = []; if ($communityAccess && $community && $canManageProfileLevels) { + $highLevelThreshold = null; + foreach ($communityLevelDefinitions as $levelDefinition) { + if ((string)($levelDefinition['label'] ?? '') === 'Säule der Väter-Community') { + $highLevelThreshold = (float)($levelDefinition['min'] ?? 0.0); + $recentHighLevelThresholdLabel = (string)($levelDefinition['label'] ?? ''); + break; + } + } + if ($highLevelThreshold !== null && $highLevelThreshold > 0) { + $recentHighLevelUsers = $community->listRecentlyReachedLevelUsers($highLevelThreshold, 30, 12); + } + $profileLevelUserSearchQuery = trim((string)($_GET['profile_level_user_query'] ?? '')); if ($profileLevelUserSearchQuery !== '') { foreach ($communityAccess->searchUsers($profileLevelUserSearchQuery, 12) as $searchRow) { @@ -1031,6 +1045,8 @@ final class AccountPages 'communityLevelRightDefinitions', 'communityRoles', 'systemRoleAssignments', + 'recentHighLevelUsers', + 'recentHighLevelThresholdLabel', 'profileLevelUserSearchQuery', 'profileLevelUserSearchResults', 'communityApplication', diff --git a/src/App/Community.php b/src/App/Community.php index 8a9ad06..e22967d 100755 --- a/src/App/Community.php +++ b/src/App/Community.php @@ -447,6 +447,99 @@ final class Community return $amount; } + public function listRecentlyReachedLevelUsers(float $minPoints, int $days = 30, int $limit = 12): array + { + if ($minPoints <= 0) { + return []; + } + + $days = max(1, min(365, $days)); + $limit = max(1, min(100, $limit)); + $cutoff = (new \DateTimeImmutable('today 23:59:59'))->modify('-' . $days . ' days'); + + $candidateStmt = $this->pdo->prepare( + 'SELECT upt.user_id, upt.total, up.display_name, up.first_name, up.last_name + FROM user_points_totals upt + JOIN users u ON u.id = upt.user_id + LEFT JOIN user_profiles up ON up.user_id = upt.user_id + WHERE upt.total >= :minPoints + ORDER BY upt.total DESC, upt.updated_at DESC + LIMIT ' . $limit + ); + $candidateStmt->execute(['minPoints' => $minPoints]); + $candidates = $candidateStmt->fetchAll(\PDO::FETCH_ASSOC) ?: []; + if ($candidates === []) { + return []; + } + + $candidateIds = array_values(array_map(static fn(array $row): int => (int)($row['user_id'] ?? 0), $candidates)); + $candidateIds = array_values(array_filter($candidateIds, static fn(int $id): bool => $id > 0)); + if ($candidateIds === []) { + return []; + } + + $placeholders = implode(',', array_fill(0, count($candidateIds), '?')); + $pointsStmt = $this->pdo->prepare( + "SELECT user_id, amount, created_at + FROM user_points + WHERE user_id IN ($placeholders) + ORDER BY user_id ASC, created_at ASC, id ASC" + ); + $pointsStmt->execute($candidateIds); + $pointRows = $pointsStmt->fetchAll(\PDO::FETCH_ASSOC) ?: []; + + $reachedAtByUser = []; + $runningTotals = []; + foreach ($pointRows as $pointRow) { + $entryUserId = (int)($pointRow['user_id'] ?? 0); + if ($entryUserId <= 0) { + continue; + } + $runningTotals[$entryUserId] = ($runningTotals[$entryUserId] ?? 0.0) + (float)($pointRow['amount'] ?? 0.0); + if (isset($reachedAtByUser[$entryUserId])) { + continue; + } + if ($runningTotals[$entryUserId] >= $minPoints) { + $reachedAtByUser[$entryUserId] = (string)($pointRow['created_at'] ?? ''); + } + } + + $recentUsers = []; + foreach ($candidates as $candidate) { + $candidateUserId = (int)($candidate['user_id'] ?? 0); + $reachedAtRaw = (string)($reachedAtByUser[$candidateUserId] ?? ''); + if ($candidateUserId <= 0 || $reachedAtRaw === '') { + continue; + } + + try { + $reachedAt = new \DateTimeImmutable($reachedAtRaw); + } catch (\Throwable) { + continue; + } + + if ($reachedAt < $cutoff) { + continue; + } + + $recentUsers[] = [ + 'user_id' => $candidateUserId, + 'display_name' => (string)($candidate['display_name'] ?? ''), + 'first_name' => (string)($candidate['first_name'] ?? ''), + 'last_name' => (string)($candidate['last_name'] ?? ''), + 'points' => (float)($candidate['total'] ?? 0.0), + 'level' => $this->membershipLevel((float)($candidate['total'] ?? 0.0)), + 'reached_at' => $reachedAt->format('Y-m-d H:i:s'), + ]; + } + + usort($recentUsers, static function (array $left, array $right): int { + return strcmp((string)($right['reached_at'] ?? ''), (string)($left['reached_at'] ?? '')); + }); + + return array_slice($recentUsers, 0, $limit); + } + public function membershipLevel(float $points): array { $level = $this->membershipLevelMeta($points); diff --git a/src/App/CommunityAccess.php b/src/App/CommunityAccess.php index 99d5a35..63c4580 100644 --- a/src/App/CommunityAccess.php +++ b/src/App/CommunityAccess.php @@ -323,14 +323,15 @@ final class CommunityAccess WHERE ' . implode(' AND ', $conditions) . ' ORDER BY CASE - WHEN LOWER(COALESCE(up.display_name, "")) = :exactQuery THEN 0 - WHEN LOWER(COALESCE(u.email, "")) = :exactQuery THEN 1 + WHEN LOWER(COALESCE(up.display_name, "")) = :exactDisplayQuery THEN 0 + WHEN LOWER(COALESCE(u.email, "")) = :exactEmailQuery THEN 1 ELSE 2 END, COALESCE(up.display_name, ""), u.id DESC LIMIT ' . $limit; - $params[':exactQuery'] = mb_strtolower($query); + $params[':exactDisplayQuery'] = mb_strtolower($query); + $params[':exactEmailQuery'] = mb_strtolower($query); $stmt = $this->pdo->prepare($sql); $stmt->execute($params);