This commit is contained in:
@@ -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.
|
- `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`.
|
- 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` 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`.
|
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
- 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`
|
- Consent- und rechtliche Texte: `public/page/datenschutz.php`
|
||||||
- System-Einstellungen: `src/App/SystemSettings.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`
|
- 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`
|
- 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`
|
||||||
|
|||||||
@@ -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.
|
- `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`.
|
- 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.
|
- 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 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
- 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`
|
- consent and legal texts: `public/page/datenschutz.php`
|
||||||
- system settings: `src/App/SystemSettings.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`
|
- 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`
|
- 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`
|
||||||
|
|||||||
@@ -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`
|
- Profil-Menü aktuell: `Profil`, `Kinder`, `Events`, `Orte & Veranstaltungen`, `Community`, `Einstellungen`, `Abmelden`
|
||||||
- Für `SiteOwner` gibt es zusätzlich den Mitgliederbereichspunkt `Profil-Levels`
|
- 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` 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
|
- 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`
|
- 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
|
||||||
|
|||||||
@@ -340,6 +340,30 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
<h2>Profil-Levels</h2>
|
<h2>Profil-Levels</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="account-panel__body">
|
<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;">
|
<div class="card" style="margin-bottom:18px;">
|
||||||
<strong>Benutzer suchen</strong>
|
<strong>Benutzer suchen</strong>
|
||||||
<form method="get" class="stack gap-12" style="margin-top:14px;">
|
<form method="get" class="stack gap-12" style="margin-top:14px;">
|
||||||
|
|||||||
@@ -938,9 +938,23 @@ final class AccountPages
|
|||||||
$communityLevelRightDefinitions = Community::membershipRightDefinitions();
|
$communityLevelRightDefinitions = Community::membershipRightDefinitions();
|
||||||
$communityRoles = $communityAccess ? $communityAccess->getUserRoles($userId) : [];
|
$communityRoles = $communityAccess ? $communityAccess->getUserRoles($userId) : [];
|
||||||
$systemRoleAssignments = $communityAccess && $canManageProfileLevels ? $communityAccess->listRoleAssignments() : [];
|
$systemRoleAssignments = $communityAccess && $canManageProfileLevels ? $communityAccess->listRoleAssignments() : [];
|
||||||
|
$recentHighLevelUsers = [];
|
||||||
|
$recentHighLevelThresholdLabel = '';
|
||||||
$profileLevelUserSearchQuery = '';
|
$profileLevelUserSearchQuery = '';
|
||||||
$profileLevelUserSearchResults = [];
|
$profileLevelUserSearchResults = [];
|
||||||
if ($communityAccess && $community && $canManageProfileLevels) {
|
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'] ?? ''));
|
$profileLevelUserSearchQuery = trim((string)($_GET['profile_level_user_query'] ?? ''));
|
||||||
if ($profileLevelUserSearchQuery !== '') {
|
if ($profileLevelUserSearchQuery !== '') {
|
||||||
foreach ($communityAccess->searchUsers($profileLevelUserSearchQuery, 12) as $searchRow) {
|
foreach ($communityAccess->searchUsers($profileLevelUserSearchQuery, 12) as $searchRow) {
|
||||||
@@ -1031,6 +1045,8 @@ final class AccountPages
|
|||||||
'communityLevelRightDefinitions',
|
'communityLevelRightDefinitions',
|
||||||
'communityRoles',
|
'communityRoles',
|
||||||
'systemRoleAssignments',
|
'systemRoleAssignments',
|
||||||
|
'recentHighLevelUsers',
|
||||||
|
'recentHighLevelThresholdLabel',
|
||||||
'profileLevelUserSearchQuery',
|
'profileLevelUserSearchQuery',
|
||||||
'profileLevelUserSearchResults',
|
'profileLevelUserSearchResults',
|
||||||
'communityApplication',
|
'communityApplication',
|
||||||
|
|||||||
@@ -447,6 +447,99 @@ final class Community
|
|||||||
return $amount;
|
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
|
public function membershipLevel(float $points): array
|
||||||
{
|
{
|
||||||
$level = $this->membershipLevelMeta($points);
|
$level = $this->membershipLevelMeta($points);
|
||||||
|
|||||||
@@ -323,14 +323,15 @@ final class CommunityAccess
|
|||||||
WHERE ' . implode(' AND ', $conditions) . '
|
WHERE ' . implode(' AND ', $conditions) . '
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN LOWER(COALESCE(up.display_name, "")) = :exactQuery THEN 0
|
WHEN LOWER(COALESCE(up.display_name, "")) = :exactDisplayQuery THEN 0
|
||||||
WHEN LOWER(COALESCE(u.email, "")) = :exactQuery THEN 1
|
WHEN LOWER(COALESCE(u.email, "")) = :exactEmailQuery THEN 1
|
||||||
ELSE 2
|
ELSE 2
|
||||||
END,
|
END,
|
||||||
COALESCE(up.display_name, ""),
|
COALESCE(up.display_name, ""),
|
||||||
u.id DESC
|
u.id DESC
|
||||||
LIMIT ' . $limit;
|
LIMIT ' . $limit;
|
||||||
$params[':exactQuery'] = mb_strtolower($query);
|
$params[':exactDisplayQuery'] = mb_strtolower($query);
|
||||||
|
$params[':exactEmailQuery'] = mb_strtolower($query);
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare($sql);
|
$stmt = $this->pdo->prepare($sql);
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
|
|||||||
Reference in New Issue
Block a user