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

This commit is contained in:
2026-08-17 00:34:41 +02:00
parent 0d384cee2d
commit 0f57f90568
9 changed files with 142 additions and 5 deletions

View File

@@ -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.

View File

@@ -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`

View File

@@ -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.

View File

@@ -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`

View File

@@ -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

View File

@@ -340,6 +340,30 @@ if (!empty($canManageSystemSettings)) {
<h2>Profil-Levels</h2>
</div>
<div class="account-panel__body">
<div class="card" style="margin-bottom:18px;">
<strong>Kürzlich erreicht</strong>
<?php if (!empty($recentHighLevelUsers) && !empty($recentHighLevelThresholdLabel)): ?>
<ul class="dash-list" style="margin-top:12px;">
<?php foreach ($recentHighLevelUsers as $recentHighLevelUser): ?>
<?php
$recentHighLevelUserName = (string)($recentHighLevelUser['display_name'] ?: trim(((string)($recentHighLevelUser['first_name'] ?? '')) . ' ' . ((string)($recentHighLevelUser['last_name'] ?? ''))) ?: 'Mitglied');
$recentHighLevelUserLevel = trim(((string)(($recentHighLevelUser['level']['icon'] ?? ''))) . ' ' . ((string)(($recentHighLevelUser['level']['label'] ?? ''))));
?>
<li>
<strong><?= htmlspecialchars($recentHighLevelUserName, ENT_QUOTES) ?></strong>
<div class="muted small" style="margin-top:4px;">
<?= htmlspecialchars($recentHighLevelUserLevel, ENT_QUOTES) ?> ·
<?= number_format((float)($recentHighLevelUser['points'] ?? 0), 1, ',', '.') ?> Punkte ·
<?= htmlspecialchars(date('d.m.Y', strtotime((string)($recentHighLevelUser['reached_at'] ?? ''))), ENT_QUOTES) ?>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p class="muted small" style="margin-top:12px;">Keine neuen Aufstiege vorhanden.</p>
<?php endif; ?>
</div>
<div class="card" style="margin-bottom:18px;">
<strong>Benutzer suchen</strong>
<form method="get" class="stack gap-12" style="margin-top:14px;">

View File

@@ -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',

View File

@@ -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);

View File

@@ -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);