This commit is contained in:
@@ -49,9 +49,11 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
||||
- 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`.
|
||||
- `Forum-Admin` kann Kategorien bereits bestätigen und zusammenführen sowie Orte und Veranstaltungen freigeben.
|
||||
- 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.
|
||||
- Kategorien liegen nicht mehr unter `System`, sondern in einem eigenen Mitgliederbereichspunkt `Kategorien`; dort werden nur neue Kategorien angezeigt und per Vorschlagsfeld mit bestehenden Kategorien aus der Datenbank zusammengeführt.
|
||||
- Neue Kategorien bleiben dort sichtbar, bis sie von einem Berechtigten bestätigt oder mit einer bestehenden Kategorie zusammengeführt werden.
|
||||
- Im Bereich `Kategorien` gibt es zusätzlich eine Suche über bestehende Kategorien, damit sie gezielt gefunden und zusammengeführt werden können.
|
||||
- Community-Level können zusätzlich die Rechte für Kategorien sowie für die Freigabe von Orten und Veranstaltungen tragen.
|
||||
|
||||
## Technischer Rahmen
|
||||
|
||||
@@ -49,9 +49,11 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
|
||||
- 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`.
|
||||
- `Forum Admin` can already confirm and merge categories as well as approve places and event-style entries.
|
||||
- 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.
|
||||
- Categories no longer live under `System`, but in their own member-area section `Categories`; that area only shows newly created categories and merges them into existing database categories through a suggestion field.
|
||||
- New categories remain visible there until an authorized user confirms them or merges them into an existing category.
|
||||
- The `Categories` area also includes a search across existing categories so they can be found and merged directly.
|
||||
- Community levels can now additionally carry the rights for category handling as well as place and event approvals.
|
||||
|
||||
## Technical Frame
|
||||
|
||||
@@ -43,6 +43,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf:
|
||||
- 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
|
||||
- Kategorien wurden aus `System` in den eigenen Mitgliederbereichspunkt `Kategorien` verschoben; dort werden nur neue Kategorien angezeigt und per Vorschlagsfeld mit bestehenden Kategorien aus der Datenbank zusammengeführt
|
||||
- Neue Kategorien bleiben im Bereich `Kategorien` sichtbar, bis sie von einem Berechtigten bestätigt oder zusammengeführt werden
|
||||
- Im Bereich `Kategorien` gibt es zusätzlich eine Suche über bestehende Kategorien, damit sie gezielt gefunden und zusammengeführt werden können
|
||||
- Community-Level können jetzt zusätzlich die Rechte für Kategorien sowie für die Freigabe von Orten und Veranstaltungen tragen
|
||||
- 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
|
||||
|
||||
@@ -959,12 +959,79 @@ if (!empty($canManageSystemSettings)) {
|
||||
<h2>Kategorien</h2>
|
||||
</div>
|
||||
<div class="account-panel__body">
|
||||
<datalist id="categoryMergeSuggestions">
|
||||
<?php foreach ($listingCategories as $targetCategory): ?>
|
||||
<option value="<?= htmlspecialchars((string)$targetCategory['title'], ENT_QUOTES) ?>"></option>
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
|
||||
<div class="card" style="margin-bottom:18px;">
|
||||
<form method="get" class="stack gap-12">
|
||||
<input type="hidden" name="section" value="categories">
|
||||
<div class="form-grid">
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="categoryQuery">Kategorie suchen</label>
|
||||
<input id="categoryQuery" name="category_query" class="input" value="<?= htmlspecialchars((string)($categorySearchQuery ?? ''), ENT_QUOTES) ?>" list="categoryMergeSuggestions">
|
||||
</div>
|
||||
<div class="stack gap-6" style="justify-content:end;">
|
||||
<span class="label" aria-hidden="true"> </span>
|
||||
<button class="btn" type="submit">Suchen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if (($categorySearchQuery ?? '') !== ''): ?>
|
||||
<div class="card" style="margin-bottom:18px;">
|
||||
<strong>Suchergebnisse</strong>
|
||||
<?php if (!empty($categorySearchResults)): ?>
|
||||
<div class="stack gap-10" style="margin-top:12px;">
|
||||
<?php foreach ($categorySearchResults as $categoryItem): ?>
|
||||
<div class="card" style="padding:12px;">
|
||||
<div>
|
||||
<strong><?= htmlspecialchars((string)$categoryItem['title'], ENT_QUOTES) ?></strong>
|
||||
<?php if (!empty($categoryItem['needs_review'])): ?>
|
||||
<span class="badge" style="margin-left:8px;">Neu</span>
|
||||
<?php endif; ?>
|
||||
<div class="muted small" style="margin-top:6px;">
|
||||
Slug: <?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?> · Einträge: <?= (int)($categoryItem['listing_count'] ?? 0) ?> · Orte: <?= (int)($categoryItem['place_count'] ?? 0) ?> · Eigene Events: <?= (int)($categoryItem['event_count'] ?? 0) ?>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" class="stack gap-8" style="margin-top:12px;">
|
||||
<input type="hidden" name="action" value="category_merge">
|
||||
<input type="hidden" name="source_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="targetCategorySearchInput<?= (int)($categoryItem['id'] ?? 0) ?>">Mit bestehender Kategorie zusammenführen</label>
|
||||
<input
|
||||
id="targetCategorySearchInput<?= (int)($categoryItem['id'] ?? 0) ?>"
|
||||
name="target_category_input"
|
||||
class="input"
|
||||
list="categoryMergeSuggestions"
|
||||
placeholder="Kategorie suchen"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="flex gap-8" style="flex-wrap:wrap;">
|
||||
<button class="btn ghost" type="submit">Zusammenführen</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php if (!empty($categoryItem['needs_review'])): ?>
|
||||
<form method="post" style="margin-top:12px;">
|
||||
<input type="hidden" name="action" value="category_confirm">
|
||||
<input type="hidden" name="source_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
|
||||
<button class="btn" type="submit">Bestätigen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="muted small" style="margin-top:12px;">Keine passenden Kategorien gefunden.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($categoryReviewItems)): ?>
|
||||
<datalist id="categoryMergeSuggestions">
|
||||
<?php foreach ($listingCategories as $targetCategory): ?>
|
||||
<option value="<?= htmlspecialchars((string)$targetCategory['title'], ENT_QUOTES) ?>"></option>
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
<div class="stack gap-10">
|
||||
<?php foreach ($categoryReviewItems as $categoryItem): ?>
|
||||
<div class="card" style="padding:12px;">
|
||||
|
||||
@@ -871,6 +871,10 @@ final class AccountPages
|
||||
$listingCatalog->listCategoryReviewItems(),
|
||||
static fn(array $item): bool => !empty($item['needs_review'])
|
||||
)) : [];
|
||||
$categorySearchQuery = $canManageCategories ? trim((string)($_GET['category_query'] ?? '')) : '';
|
||||
$categorySearchResults = ($listingCatalog && $canManageCategories && $categorySearchQuery !== '')
|
||||
? $listingCatalog->searchCategories($categorySearchQuery, 20)
|
||||
: [];
|
||||
$placeSuggestions = $listingCatalog ? $listingCatalog->listPlaceSuggestions(60) : [];
|
||||
$eventLocationOptions = $listingCatalog ? $listingCatalog->listEventLocationOptions(120) : [];
|
||||
$stmt = $pdo?->prepare(
|
||||
@@ -1000,6 +1004,7 @@ final class AccountPages
|
||||
'Themen und Antworten moderieren',
|
||||
'Community-Sperren setzen und aufheben',
|
||||
'Meldungen bearbeiten',
|
||||
'Kategorien bestätigen und zusammenführen',
|
||||
'Orte und Veranstaltungen freigeben',
|
||||
],
|
||||
],
|
||||
@@ -1009,7 +1014,6 @@ final class AccountPages
|
||||
'rights' => [
|
||||
'Beinhaltet alle Rechte von Forum-Admin',
|
||||
'Community-Admin-Bewerbungen bearbeiten',
|
||||
'Kategorien bestätigen und zusammenführen',
|
||||
'System-Einstellungen verwalten',
|
||||
],
|
||||
],
|
||||
@@ -1057,6 +1061,8 @@ final class AccountPages
|
||||
'editListing',
|
||||
'listingCategories',
|
||||
'categoryReviewItems',
|
||||
'categorySearchQuery',
|
||||
'categorySearchResults',
|
||||
'placeSuggestions',
|
||||
'eventLocationOptions',
|
||||
'communityPoints',
|
||||
|
||||
@@ -77,7 +77,7 @@ final class CommunityAccess
|
||||
|
||||
public function canManageCategories(int $userId, ?float $points = null): bool
|
||||
{
|
||||
if ($this->hasRole($userId, 'owner') || $this->hasRole($userId, 'site_admin')) {
|
||||
if ($this->hasRole($userId, 'owner') || $this->hasRole($userId, 'site_admin') || $this->hasRole($userId, 'forum_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -314,6 +314,68 @@ final class ListingCatalog
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function searchCategories(string $query, int $limit = 20): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$query = trim($query);
|
||||
if ($query === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$limit = max(1, min(100, $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) {
|
||||
$titleKey = ':title_' . $index;
|
||||
$slugKey = ':slug_' . $index;
|
||||
$conditions[] = '(LOWER(c.title) LIKE ' . $titleKey . ' OR LOWER(c.slug) LIKE ' . $slugKey . ')';
|
||||
$params[$titleKey] = '%' . $token . '%';
|
||||
$params[$slugKey] = '%' . $token . '%';
|
||||
}
|
||||
|
||||
$sql = '
|
||||
SELECT c.id, c.slug, c.title, c.category_group, c.sort_order,
|
||||
(SELECT COUNT(*) FROM listing_category_map m WHERE m.category_id = c.id) AS listing_count,
|
||||
(SELECT COUNT(*) FROM listing_places p WHERE p.place_kind = c.slug) AS place_count
|
||||
FROM listing_categories c
|
||||
WHERE ' . implode(' AND ', $conditions) . '
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN LOWER(c.title) = :exactTitle THEN 0
|
||||
WHEN LOWER(c.slug) = :exactSlug THEN 1
|
||||
WHEN c.sort_order >= 900 THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
c.sort_order ASC,
|
||||
c.title ASC
|
||||
LIMIT ' . $limit;
|
||||
$params[':exactTitle'] = mb_strtolower($query);
|
||||
$params[':exactSlug'] = $this->slugify($query);
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
$eventStmt = $this->pdo->query('SELECT category_slug, COUNT(*) AS total FROM events WHERE category_slug IS NOT NULL AND category_slug <> "" GROUP BY category_slug');
|
||||
$eventCounts = [];
|
||||
foreach (($eventStmt ? $eventStmt->fetchAll(\PDO::FETCH_ASSOC) : []) as $row) {
|
||||
$eventCounts[(string)$row['category_slug']] = (int)$row['total'];
|
||||
}
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['event_count'] = $eventCounts[(string)$row['slug']] ?? 0;
|
||||
$row['needs_review'] = ((int)($row['sort_order'] ?? 0)) >= 900;
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function mergeCategories(string $sourceSlug, string $targetSlug): void
|
||||
{
|
||||
$this->ensureSchema();
|
||||
|
||||
Reference in New Issue
Block a user