This commit is contained in:
@@ -17,6 +17,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
||||
|
||||
## Konto / Mitgliederbereich
|
||||
- Profil-Menü aktuell: `Profil`, `Kinder`, `Events`, `Orte & Veranstaltungen`, `Community`, `Einstellungen`, `Abmelden`
|
||||
- Für `SiteOwner` kommt zusätzlich der Bereich `Profil-Levels` dazu.
|
||||
- Kinder können angelegt, bearbeitet und gelöscht werden.
|
||||
- Wenn ein Geburtsdatum gesetzt ist, wird das Alter automatisch berechnet und später bei Bedarf aktualisiert.
|
||||
- Die Konto-E-Mail wird app-seitig verschlüsselt gespeichert und über einen separaten HMAC-Lookup-Hash adressiert.
|
||||
@@ -43,6 +44,9 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
||||
- Bei `Veranstaltung aus Datenbank` muss das Event-Datum innerhalb des hinterlegten Gültigkeitszeitraums dieser Veranstaltungs-Quelle liegen.
|
||||
- Kategorien werden im Mitgliederbereich per Sucheingabe mit bestehenden Vorschlägen und automatischer Neuanlage gepflegt.
|
||||
- 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`.
|
||||
- System-Level bleiben getrennt als Rollen `Forum-Admin`, `Site-Admin` und `SiteOwner`.
|
||||
|
||||
## Technischer Rahmen
|
||||
- eigener Front-Controller in `public/index.php`
|
||||
|
||||
@@ -20,6 +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: `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`
|
||||
|
||||
@@ -17,6 +17,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
|
||||
- standortbasierte Sortierung für die neuesten Events
|
||||
- Mitgliederbereich mit linker Bereichsnavigation
|
||||
- Profil-Menü mit `Profil`, `Kinder`, `Events`, `Orte & Veranstaltungen`, `Community`, `Einstellungen`
|
||||
- Für `SiteOwner` gibt es zusätzlich den Punkt `Profil-Levels`
|
||||
- Konto-E-Mails sowie sensible Profilfelder werden app-seitig verschlüsselt gespeichert
|
||||
- Kinder können angelegt, bearbeitet und gelöscht werden
|
||||
- Profiladresse mit verschlüsselter Straße/Hausnummer, Adresssuche, Browser-Übernahme und Validierung
|
||||
|
||||
@@ -17,6 +17,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
|
||||
|
||||
## Account / Member Area
|
||||
- Profile menu currently contains `Profile`, `Children`, `Events`, `Places & Events`, `Community`, `Settings`, `Logout`.
|
||||
- `SiteOwner` additionally gets the area `Profile Levels`.
|
||||
- Children can be created, edited, and deleted.
|
||||
- If a birth date is set, age is calculated automatically and updated later when needed.
|
||||
- Account email is encrypted application-side and addressed through a separate HMAC lookup hash.
|
||||
@@ -43,6 +44,9 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
|
||||
- When `event from database` is selected, the event date must fall inside that source event's valid date window.
|
||||
- Categories in the member area are handled via a search input with existing suggestions and automatic creation when needed.
|
||||
- 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`.
|
||||
- System levels remain separate as the roles `Forum Admin`, `Site Admin`, and `SiteOwner`.
|
||||
|
||||
## Technical Frame
|
||||
- custom front controller in `public/index.php`
|
||||
|
||||
@@ -20,6 +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: `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`
|
||||
|
||||
@@ -17,6 +17,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers focused on local events, app
|
||||
- location-based ordering for the newest events
|
||||
- member area with left-side section navigation
|
||||
- profile menu with `Profile`, `Children`, `Events`, `Places & Events`, `Community`, `Settings`
|
||||
- `SiteOwner` additionally gets the member-area item `Profile Levels`
|
||||
- account emails and sensitive profile fields are stored encrypted application-side
|
||||
- children can be created, edited, and deleted
|
||||
- profile address supports encrypted street/house number, address search, browser import, and validation
|
||||
|
||||
@@ -37,6 +37,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf:
|
||||
- `Termine` und `Treffen` ergänzend in SEO- und Erklärungstexten
|
||||
- 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`
|
||||
- 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
|
||||
|
||||
@@ -142,6 +142,9 @@ $sectionLinks = [
|
||||
'community' => 'Community',
|
||||
'settings' => 'Einstellungen',
|
||||
];
|
||||
if (!empty($canManageProfileLevels)) {
|
||||
$sectionLinks['profile-levels'] = 'Profil-Levels';
|
||||
}
|
||||
$showEventTypeChoice = !$editing && (($section ?? 'events') === 'places') && $requestedCreateListingKind === null;
|
||||
$modalSectionTarget = ($editingListing || ($section ?? '') === 'places') ? 'places' : 'events';
|
||||
if (!empty($canManageSystemSettings)) {
|
||||
@@ -321,6 +324,141 @@ if (!empty($canManageSystemSettings)) {
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($section === 'profile-levels' && !empty($canManageProfileLevels)): ?>
|
||||
<?php
|
||||
$systemRoleAssignmentsByRole = [];
|
||||
foreach (($systemRoleAssignments ?? []) as $assignment) {
|
||||
$roleKey = (string)($assignment['role'] ?? '');
|
||||
if ($roleKey === '') {
|
||||
continue;
|
||||
}
|
||||
$systemRoleAssignmentsByRole[$roleKey][] = $assignment;
|
||||
}
|
||||
?>
|
||||
<section class="account-panel">
|
||||
<div class="account-panel__head">
|
||||
<h2>Profil-Levels</h2>
|
||||
</div>
|
||||
<div class="account-panel__body">
|
||||
<div class="card">
|
||||
<strong>Community-Levels</strong>
|
||||
<div class="stack gap-12" style="margin-top:14px;">
|
||||
<?php foreach (($communityLevelDefinitions ?? []) as $levelDefinition): ?>
|
||||
<details class="card" style="padding:14px;">
|
||||
<summary style="cursor:pointer; font-weight:700;">
|
||||
<?= htmlspecialchars((string)($levelDefinition['label'] ?? ''), ENT_QUOTES) ?> · ab <?= number_format((float)($levelDefinition['min'] ?? 0), 1, ',', '.') ?> Punkten
|
||||
</summary>
|
||||
<form method="post" class="stack gap-12" style="margin-top:14px;">
|
||||
<input type="hidden" name="action" value="community_level_save">
|
||||
<input type="hidden" name="level_id" value="<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">
|
||||
<div class="form-grid">
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="communityLevelLabel<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">Name</label>
|
||||
<input id="communityLevelLabel<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>" name="label" class="input" value="<?= htmlspecialchars((string)($levelDefinition['label'] ?? ''), ENT_QUOTES) ?>" required>
|
||||
</div>
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="communityLevelMin<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">Mindestpunkte</label>
|
||||
<input id="communityLevelMin<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>" name="min_points" class="input" type="number" min="0" step="0.1" value="<?= htmlspecialchars((string)($levelDefinition['min'] ?? '0'), ENT_QUOTES) ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="communityLevelIcon<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">Icon (optional)</label>
|
||||
<input id="communityLevelIcon<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>" name="icon" class="input" value="<?= htmlspecialchars((string)($levelDefinition['icon'] ?? ''), ENT_QUOTES) ?>">
|
||||
</div>
|
||||
<div class="stack gap-8">
|
||||
<span class="label">Rechte</span>
|
||||
<div class="chips" style="flex-wrap:wrap;">
|
||||
<?php foreach (($communityLevelRightDefinitions ?? []) as $rightKey => $rightDefinition): ?>
|
||||
<label class="chip" style="cursor:pointer;">
|
||||
<input type="hidden" name="rights[<?= htmlspecialchars((string)$rightKey, ENT_QUOTES) ?>]" value="0">
|
||||
<input type="checkbox" name="rights[<?= htmlspecialchars((string)$rightKey, ENT_QUOTES) ?>]" value="1" <?= !empty($levelDefinition['rights'][$rightKey]) ? 'checked' : '' ?> style="margin-right:6px;">
|
||||
<?= htmlspecialchars((string)($rightDefinition['label'] ?? $rightKey), ENT_QUOTES) ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-12" style="flex-wrap:wrap;">
|
||||
<button class="btn" type="submit">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" onsubmit="return confirm('Community-Level wirklich löschen?');">
|
||||
<input type="hidden" name="action" value="community_level_delete">
|
||||
<input type="hidden" name="level_id" value="<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">
|
||||
<button class="btn ghost" type="submit">Löschen</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:18px;">
|
||||
<strong>Neues Community-Level</strong>
|
||||
<form method="post" class="stack gap-12" style="margin-top:14px;">
|
||||
<input type="hidden" name="action" value="community_level_save">
|
||||
<input type="hidden" name="level_id" value="">
|
||||
<div class="form-grid">
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="communityLevelLabelNew">Name</label>
|
||||
<input id="communityLevelLabelNew" name="label" class="input" required>
|
||||
</div>
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="communityLevelMinNew">Mindestpunkte</label>
|
||||
<input id="communityLevelMinNew" name="min_points" class="input" type="number" min="0" step="0.1" value="0" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="communityLevelIconNew">Icon (optional)</label>
|
||||
<input id="communityLevelIconNew" name="icon" class="input">
|
||||
</div>
|
||||
<div class="stack gap-8">
|
||||
<span class="label">Rechte</span>
|
||||
<div class="chips" style="flex-wrap:wrap;">
|
||||
<?php foreach (($communityLevelRightDefinitions ?? []) as $rightKey => $rightDefinition): ?>
|
||||
<label class="chip" style="cursor:pointer;">
|
||||
<input type="hidden" name="rights[<?= htmlspecialchars((string)$rightKey, ENT_QUOTES) ?>]" value="0">
|
||||
<input type="checkbox" name="rights[<?= htmlspecialchars((string)$rightKey, ENT_QUOTES) ?>]" value="1" style="margin-right:6px;">
|
||||
<?= htmlspecialchars((string)($rightDefinition['label'] ?? $rightKey), ENT_QUOTES) ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-12">
|
||||
<button class="btn" type="submit">Community-Level anlegen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:18px;">
|
||||
<strong>System-Level</strong>
|
||||
<div class="stack gap-12" style="margin-top:14px;">
|
||||
<?php foreach (($systemLevelDefinitions ?? []) as $systemLevel): ?>
|
||||
<div class="card" style="padding:14px;">
|
||||
<strong><?= htmlspecialchars((string)($systemLevel['label'] ?? ''), ENT_QUOTES) ?></strong>
|
||||
<?php if (!empty($systemRoleAssignmentsByRole[(string)($systemLevel['key'] ?? '')])): ?>
|
||||
<div class="muted small" style="margin-top:8px;">
|
||||
Aktuell:
|
||||
<?php
|
||||
$assignmentLabels = [];
|
||||
foreach ($systemRoleAssignmentsByRole[(string)$systemLevel['key']] as $assignment) {
|
||||
$assignmentLabels[] = (string)($assignment['display_name'] ?? 'Mitglied');
|
||||
}
|
||||
echo htmlspecialchars(implode(', ', $assignmentLabels), ENT_QUOTES);
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<ul class="dash-list" style="margin-top:12px;">
|
||||
<?php foreach (($systemLevel['rights'] ?? []) as $systemRight): ?>
|
||||
<li><?= htmlspecialchars((string)$systemRight, ENT_QUOTES) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($section === 'events'): ?>
|
||||
<section class="account-panel" id="events">
|
||||
<div class="account-panel__head">
|
||||
|
||||
@@ -78,7 +78,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $community && $access) {
|
||||
}
|
||||
$points = $community->computePoints((int)$userId);
|
||||
if (!$access->canHighlightHelpful($points) && !$access->canModerateForum((int)$userId)) {
|
||||
throw new \RuntimeException('Dafür ist mindestens der Rang Mentor-Vater erforderlich.');
|
||||
throw new \RuntimeException('Dein Community-Level darf hilfreiche Antworten aktuell noch nicht hervorheben.');
|
||||
}
|
||||
$postId = (int)($_POST['post_id'] ?? 0);
|
||||
$highlight = ((string)($_POST['highlight'] ?? '1')) === '1';
|
||||
|
||||
@@ -5,6 +5,7 @@ $displayName = 'Profil';
|
||||
$profileInitial = 'P';
|
||||
$showAdminLink = false;
|
||||
$showSystemLink = false;
|
||||
$showProfileLevelsLink = false;
|
||||
|
||||
if ($isLoggedIn) {
|
||||
try {
|
||||
@@ -27,6 +28,7 @@ if ($isLoggedIn) {
|
||||
$communityAccess = new \App\CommunityAccess($pdo, $communityConfig);
|
||||
$showAdminLink = $communityAccess->canModerateForum((int)$_SESSION['user_id']);
|
||||
$showSystemLink = $communityAccess->canManageApplications((int)$_SESSION['user_id']);
|
||||
$showProfileLevelsLink = $communityAccess->canManageRoles((int)$_SESSION['user_id']);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
@@ -34,6 +36,7 @@ if ($isLoggedIn) {
|
||||
$profileInitial = 'P';
|
||||
$showAdminLink = false;
|
||||
$showSystemLink = false;
|
||||
$showProfileLevelsLink = false;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -65,6 +68,9 @@ if ($isLoggedIn) {
|
||||
<a href="/dashboard?section=places" role="menuitem">Orte & Veranstaltungen</a>
|
||||
<a href="/dashboard?section=community" role="menuitem">Community</a>
|
||||
<a href="/dashboard?section=settings" role="menuitem">Einstellungen</a>
|
||||
<?php if ($showProfileLevelsLink): ?>
|
||||
<a href="/dashboard?section=profile-levels" role="menuitem">Profil-Levels</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($showSystemLink): ?>
|
||||
<a href="/dashboard?section=system" role="menuitem">System</a>
|
||||
<?php endif; ?>
|
||||
@@ -92,6 +98,9 @@ if ($isLoggedIn) {
|
||||
<a class="btn ghost" href="/dashboard?section=places">Orte & Veranstaltungen</a>
|
||||
<a class="btn ghost" href="/dashboard?section=community">Community</a>
|
||||
<a class="btn ghost" href="/dashboard?section=settings">Einstellungen</a>
|
||||
<?php if ($showProfileLevelsLink): ?>
|
||||
<a class="btn ghost" href="/dashboard?section=profile-levels">Profil-Levels</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($showSystemLink): ?>
|
||||
<a class="btn ghost" href="/dashboard?section=system">System</a>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -179,10 +179,14 @@ final class AccountPages
|
||||
$calendarSync = $pdo ? new CalendarSync($app) : null;
|
||||
$section = (string)($_GET['section'] ?? 'profile');
|
||||
$canManageSystemSettings = $communityAccess ? $communityAccess->canManageApplications($userId) : false;
|
||||
$canManageProfileLevels = $communityAccess ? $communityAccess->canManageRoles($userId) : false;
|
||||
$allowedSections = ['profile', 'children', 'events', 'places', 'community', 'settings'];
|
||||
if ($canManageSystemSettings) {
|
||||
$allowedSections[] = 'system';
|
||||
}
|
||||
if ($canManageProfileLevels) {
|
||||
$allowedSections[] = 'profile-levels';
|
||||
}
|
||||
|
||||
if ($systemSettings) {
|
||||
$systemSettings->ensureSchema();
|
||||
@@ -641,6 +645,68 @@ final class AccountPages
|
||||
}
|
||||
$communityAccess->submitApplication($userId, (string)($_POST['motivation'] ?? ''));
|
||||
$info = 'Deine Bewerbung wurde eingereicht.';
|
||||
} elseif ($action === 'community_level_save') {
|
||||
if (!$canManageProfileLevels || !$community) {
|
||||
throw new \RuntimeException('Keine Berechtigung für Profil-Levels.');
|
||||
}
|
||||
|
||||
$levelId = trim((string)($_POST['level_id'] ?? ''));
|
||||
$label = trim((string)($_POST['label'] ?? ''));
|
||||
$minValueRaw = trim((string)($_POST['min_points'] ?? '0'));
|
||||
$icon = trim((string)($_POST['icon'] ?? ''));
|
||||
if ($label === '') {
|
||||
throw new \RuntimeException('Bitte gib einen Namen für das Community-Level an.');
|
||||
}
|
||||
if ($minValueRaw === '' || !is_numeric($minValueRaw)) {
|
||||
throw new \RuntimeException('Bitte gib eine gültige Mindestpunktzahl an.');
|
||||
}
|
||||
|
||||
$rights = [];
|
||||
foreach (array_keys(Community::membershipRightDefinitions()) as $rightKey) {
|
||||
$rights[$rightKey] = ((string)($_POST['rights'][$rightKey] ?? '0')) === '1';
|
||||
}
|
||||
|
||||
$levels = $community->listMembershipLevels();
|
||||
$updated = false;
|
||||
foreach ($levels as &$level) {
|
||||
if ((string)($level['id'] ?? '') !== $levelId) {
|
||||
continue;
|
||||
}
|
||||
$level['label'] = $label;
|
||||
$level['min'] = (float)$minValueRaw;
|
||||
$level['icon'] = $icon;
|
||||
$level['rights'] = $rights;
|
||||
$updated = true;
|
||||
break;
|
||||
}
|
||||
unset($level);
|
||||
|
||||
if (!$updated) {
|
||||
$levels[] = [
|
||||
'id' => $levelId !== '' ? $levelId : self::slugifyValue($label . '-' . $minValueRaw . '-' . bin2hex(random_bytes(3))),
|
||||
'label' => $label,
|
||||
'min' => (float)$minValueRaw,
|
||||
'icon' => $icon,
|
||||
'rights' => $rights,
|
||||
];
|
||||
}
|
||||
|
||||
$community->saveMembershipLevels($levels, $userId);
|
||||
$info = $updated ? 'Community-Level gespeichert.' : 'Community-Level angelegt.';
|
||||
} elseif ($action === 'community_level_delete') {
|
||||
if (!$canManageProfileLevels || !$community) {
|
||||
throw new \RuntimeException('Keine Berechtigung für Profil-Levels.');
|
||||
}
|
||||
$levelId = trim((string)($_POST['level_id'] ?? ''));
|
||||
$levels = array_values(array_filter(
|
||||
$community->listMembershipLevels(),
|
||||
static fn(array $level): bool => (string)($level['id'] ?? '') !== $levelId
|
||||
));
|
||||
if ($levels === []) {
|
||||
throw new \RuntimeException('Mindestens ein Community-Level muss erhalten bleiben.');
|
||||
}
|
||||
$community->saveMembershipLevels($levels, $userId);
|
||||
$info = 'Community-Level gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
@@ -843,7 +909,10 @@ final class AccountPages
|
||||
|
||||
$communityPoints = $community ? $community->computePoints($userId) : 0.0;
|
||||
$communityLevel = $community ? $community->membershipLevel($communityPoints) : ['label' => '', 'icon' => ''];
|
||||
$communityLevelDefinitions = $community ? $community->listMembershipLevels() : [];
|
||||
$communityLevelRightDefinitions = Community::membershipRightDefinitions();
|
||||
$communityRoles = $communityAccess ? $communityAccess->getUserRoles($userId) : [];
|
||||
$systemRoleAssignments = $communityAccess && $canManageProfileLevels ? $communityAccess->listRoleAssignments() : [];
|
||||
$communityApplication = $communityAccess ? $communityAccess->getLatestApplication($userId) : null;
|
||||
$communityCanApply = $communityAccess ? $communityAccess->canApplyForForumAdmin($userId, $communityPoints) : false;
|
||||
$communityRestrictions = $communityAccess ? $communityAccess->getRestrictionState($userId) : [
|
||||
@@ -851,6 +920,35 @@ final class AccountPages
|
||||
'reply_blocked' => false,
|
||||
'reason' => null,
|
||||
];
|
||||
$systemLevelDefinitions = [
|
||||
[
|
||||
'key' => 'forum_admin',
|
||||
'label' => 'Forum-Admin',
|
||||
'rights' => [
|
||||
'Themen und Antworten moderieren',
|
||||
'Community-Sperren setzen und aufheben',
|
||||
'Meldungen bearbeiten',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'site_admin',
|
||||
'label' => 'Site-Admin',
|
||||
'rights' => [
|
||||
'Beinhaltet alle Rechte von Forum-Admin',
|
||||
'Community-Admin-Bewerbungen bearbeiten',
|
||||
'System-Einstellungen verwalten',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'owner',
|
||||
'label' => 'SiteOwner',
|
||||
'rights' => [
|
||||
'Beinhaltet alle Rechte von Site-Admin',
|
||||
'System-Rollen vergeben und entziehen',
|
||||
'Profil-Levels verwalten',
|
||||
],
|
||||
],
|
||||
];
|
||||
$systemSettingsValues = $systemSettings ? $systemSettings->getAll() : [];
|
||||
$listingCatalogStatus = $listingCatalog ? $listingCatalog->status() : ['complete' => false, 'missing' => [], 'tables' => []];
|
||||
if (!in_array($section, $allowedSections, true)) {
|
||||
@@ -889,11 +987,16 @@ final class AccountPages
|
||||
'eventLocationOptions',
|
||||
'communityPoints',
|
||||
'communityLevel',
|
||||
'communityLevelDefinitions',
|
||||
'communityLevelRightDefinitions',
|
||||
'communityRoles',
|
||||
'systemRoleAssignments',
|
||||
'communityApplication',
|
||||
'communityCanApply',
|
||||
'communityRestrictions',
|
||||
'systemLevelDefinitions',
|
||||
'canManageSystemSettings',
|
||||
'canManageProfileLevels',
|
||||
'systemSettingsValues',
|
||||
'listingCatalogStatus',
|
||||
'avatarBuilder',
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Avatar\AvatarManager;
|
||||
final class Community
|
||||
{
|
||||
private ?array $forumStructure = null;
|
||||
private ?array $membershipLevelCache = null;
|
||||
private array $tableCache = [];
|
||||
private array $columnCache = [];
|
||||
|
||||
@@ -418,18 +419,122 @@ final class Community
|
||||
|
||||
public function membershipLevel(float $points): array
|
||||
{
|
||||
$levels = $this->config['levels'] ?? [];
|
||||
usort($levels, fn($a,$b) => ($b['min'] ?? 0) <=> ($a['min'] ?? 0));
|
||||
foreach ($levels as $lvl) {
|
||||
if ($points >= (float)($lvl['min'] ?? 0)) {
|
||||
return [
|
||||
'label' => $lvl['label'] ?? 'New Daddy',
|
||||
'icon' => $lvl['icon'] ?? '',
|
||||
];
|
||||
$level = $this->membershipLevelMeta($points);
|
||||
return [
|
||||
'label' => (string)($level['label'] ?? 'Neuer Vater'),
|
||||
'icon' => (string)($level['icon'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
public function membershipLevelMeta(float $points): array
|
||||
{
|
||||
$levels = $this->listMembershipLevels();
|
||||
$sortedLevels = $levels;
|
||||
usort($sortedLevels, fn(array $a, array $b): int => ((float)($b['min'] ?? 0.0)) <=> ((float)($a['min'] ?? 0.0)));
|
||||
foreach ($sortedLevels as $level) {
|
||||
if ($points >= (float)($level['min'] ?? 0.0)) {
|
||||
return $level;
|
||||
}
|
||||
}
|
||||
$fallback = $levels ? $levels[count($levels)-1] : ['label' => 'New Daddy','icon' => ''];
|
||||
return ['label' => $fallback['label'], 'icon' => $fallback['icon'] ?? ''];
|
||||
|
||||
return $sortedLevels !== [] ? $sortedLevels[array_key_last($sortedLevels)] : $this->normalizeMembershipLevel(['label' => 'Neuer Vater', 'min' => 0], 0);
|
||||
}
|
||||
|
||||
public function listMembershipLevels(): array
|
||||
{
|
||||
if ($this->membershipLevelCache !== null) {
|
||||
return $this->membershipLevelCache;
|
||||
}
|
||||
|
||||
$levels = $this->config['levels'] ?? [];
|
||||
try {
|
||||
$settings = new SystemSettings($this->pdo);
|
||||
$rawLevels = trim((string)$settings->get('community_levels_json', ''));
|
||||
if ($rawLevels !== '') {
|
||||
$decodedLevels = json_decode($rawLevels, true);
|
||||
if (is_array($decodedLevels)) {
|
||||
$levels = $decodedLevels;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
$normalizedLevels = [];
|
||||
foreach ($levels as $index => $level) {
|
||||
if (!is_array($level)) {
|
||||
continue;
|
||||
}
|
||||
$normalizedLevels[] = $this->normalizeMembershipLevel($level, (int)$index);
|
||||
}
|
||||
|
||||
if ($normalizedLevels === []) {
|
||||
$normalizedLevels[] = $this->normalizeMembershipLevel(['label' => 'Neuer Vater', 'min' => 0], 0);
|
||||
}
|
||||
|
||||
usort($normalizedLevels, function (array $left, array $right): int {
|
||||
$minCompare = ((float)($left['min'] ?? 0.0)) <=> ((float)($right['min'] ?? 0.0));
|
||||
if ($minCompare !== 0) {
|
||||
return $minCompare;
|
||||
}
|
||||
|
||||
return strcmp((string)($left['label'] ?? ''), (string)($right['label'] ?? ''));
|
||||
});
|
||||
|
||||
return $this->membershipLevelCache = array_values($normalizedLevels);
|
||||
}
|
||||
|
||||
public function saveMembershipLevels(array $levels, ?int $updatedBy = null): void
|
||||
{
|
||||
$normalizedLevels = [];
|
||||
foreach ($levels as $index => $level) {
|
||||
if (!is_array($level)) {
|
||||
continue;
|
||||
}
|
||||
$normalizedLevels[] = $this->normalizeMembershipLevel($level, (int)$index);
|
||||
}
|
||||
|
||||
if ($normalizedLevels === []) {
|
||||
throw new \RuntimeException('Mindestens ein Community-Level muss vorhanden sein.');
|
||||
}
|
||||
|
||||
$seenIds = [];
|
||||
foreach ($normalizedLevels as $level) {
|
||||
$levelId = (string)($level['id'] ?? '');
|
||||
if ($levelId === '' || isset($seenIds[$levelId])) {
|
||||
throw new \RuntimeException('Jedes Community-Level braucht eine eindeutige Kennung.');
|
||||
}
|
||||
$seenIds[$levelId] = true;
|
||||
}
|
||||
|
||||
usort($normalizedLevels, function (array $left, array $right): int {
|
||||
$minCompare = ((float)($left['min'] ?? 0.0)) <=> ((float)($right['min'] ?? 0.0));
|
||||
if ($minCompare !== 0) {
|
||||
return $minCompare;
|
||||
}
|
||||
|
||||
return strcmp((string)($left['label'] ?? ''), (string)($right['label'] ?? ''));
|
||||
});
|
||||
|
||||
$settings = new SystemSettings($this->pdo);
|
||||
$settings->ensureSchema();
|
||||
$settings->set(
|
||||
'community_levels_json',
|
||||
json_encode($normalizedLevels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) ?: '[]',
|
||||
$updatedBy
|
||||
);
|
||||
$this->membershipLevelCache = array_values($normalizedLevels);
|
||||
}
|
||||
|
||||
public static function membershipRightDefinitions(): array
|
||||
{
|
||||
return [
|
||||
'can_highlight_helpful' => [
|
||||
'label' => 'Hilfreiche Antworten hervorheben',
|
||||
],
|
||||
'can_apply_for_forum_admin' => [
|
||||
'label' => 'Bewerbung als Forum-Admin',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function forumStructure(): array
|
||||
@@ -554,4 +659,63 @@ final class Community
|
||||
|
||||
return implode(', ', $columns);
|
||||
}
|
||||
|
||||
private function normalizeMembershipLevel(array $level, int $index): array
|
||||
{
|
||||
$label = trim((string)($level['label'] ?? ''));
|
||||
if ($label === '') {
|
||||
$label = 'Level ' . ($index + 1);
|
||||
}
|
||||
|
||||
$minValue = is_numeric($level['min'] ?? null) ? (float)$level['min'] : 0.0;
|
||||
if ($minValue < 0) {
|
||||
$minValue = 0.0;
|
||||
}
|
||||
|
||||
$levelId = trim((string)($level['id'] ?? ''));
|
||||
if ($levelId === '') {
|
||||
$levelId = $this->slugifyLevelIdentifier($label . '-' . (string)$minValue . '-' . (string)$index);
|
||||
}
|
||||
|
||||
$rightsInput = is_array($level['rights'] ?? null) ? $level['rights'] : [];
|
||||
$rights = [];
|
||||
foreach (array_keys(self::membershipRightDefinitions()) as $rightKey) {
|
||||
if (array_key_exists($rightKey, $rightsInput)) {
|
||||
$rights[$rightKey] = $this->toBool($rightsInput[$rightKey]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$rights[$rightKey] = match ($rightKey) {
|
||||
'can_highlight_helpful' => $minValue >= 500.0,
|
||||
'can_apply_for_forum_admin' => $minValue >= 750.0,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $levelId,
|
||||
'min' => $minValue,
|
||||
'label' => $label,
|
||||
'icon' => trim((string)($level['icon'] ?? '')),
|
||||
'rights' => $rights,
|
||||
];
|
||||
}
|
||||
|
||||
private function slugifyLevelIdentifier(string $value): string
|
||||
{
|
||||
$value = mb_strtolower(trim($value));
|
||||
$value = strtr($value, ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss']);
|
||||
$value = preg_replace('/[^a-z0-9]+/u', '-', $value) ?: '';
|
||||
$value = trim($value, '-');
|
||||
return $value !== '' ? $value : 'community-level';
|
||||
}
|
||||
|
||||
private function toBool(mixed $value): bool
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return in_array((string)$value, ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,10 @@ final class CommunityAccess
|
||||
|
||||
public function canApplyForForumAdmin(int $userId, float $points): bool
|
||||
{
|
||||
if ($userId <= 0 || $points < 750.0 || !$this->hasTable('community_admin_applications')) {
|
||||
if ($userId <= 0 || !$this->hasTable('community_admin_applications')) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->resolveLevelRight($points, 'can_apply_for_forum_admin')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -450,7 +453,7 @@ final class CommunityAccess
|
||||
|
||||
public function canHighlightHelpful(float $points): bool
|
||||
{
|
||||
return $points >= 500.0;
|
||||
return $this->resolveLevelRight($points, 'can_highlight_helpful');
|
||||
}
|
||||
|
||||
public function supportsApplications(): bool
|
||||
@@ -492,4 +495,11 @@ final class CommunityAccess
|
||||
return $this->tableCache[$table] = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveLevelRight(float $points, string $rightKey): bool
|
||||
{
|
||||
$community = new Community($this->pdo, $this->communityConfig);
|
||||
$level = $community->membershipLevelMeta($points);
|
||||
return !empty($level['rights'][$rightKey]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ final class SystemSettings
|
||||
'site_maintenance_mode' => '0',
|
||||
'site_maintenance_message' => 'Papa-Kind-Treff ist gerade kurz in Wartung. Bitte versuche es in Kürze erneut.',
|
||||
'place_data_provider' => 'osm',
|
||||
'community_levels_json' => '',
|
||||
];
|
||||
|
||||
public function __construct(private \PDO $pdo)
|
||||
|
||||
Reference in New Issue
Block a user