yyxc
All checks were successful
Deploy / deploy (push) Successful in 56s

This commit is contained in:
2026-08-19 20:14:15 +02:00
parent c567d19deb
commit 39dfc97ae6
7 changed files with 147 additions and 7 deletions

View File

@@ -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` 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. - 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`.
- `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. - 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. - 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. - 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. - Community-Level können zusätzlich die Rechte für Kategorien sowie für die Freigabe von Orten und Veranstaltungen tragen.
## Technischer Rahmen ## Technischer Rahmen

View File

@@ -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. - 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. - `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`.
- `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. - 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. - 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. - 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. - Community levels can now additionally carry the rights for category handling as well as place and event approvals.
## Technical Frame ## Technical Frame

View File

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

View File

@@ -959,12 +959,79 @@ if (!empty($canManageSystemSettings)) {
<h2>Kategorien</h2> <h2>Kategorien</h2>
</div> </div>
<div class="account-panel__body"> <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">&nbsp;</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)): ?> <?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"> <div class="stack gap-10">
<?php foreach ($categoryReviewItems as $categoryItem): ?> <?php foreach ($categoryReviewItems as $categoryItem): ?>
<div class="card" style="padding:12px;"> <div class="card" style="padding:12px;">

View File

@@ -871,6 +871,10 @@ final class AccountPages
$listingCatalog->listCategoryReviewItems(), $listingCatalog->listCategoryReviewItems(),
static fn(array $item): bool => !empty($item['needs_review']) 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) : []; $placeSuggestions = $listingCatalog ? $listingCatalog->listPlaceSuggestions(60) : [];
$eventLocationOptions = $listingCatalog ? $listingCatalog->listEventLocationOptions(120) : []; $eventLocationOptions = $listingCatalog ? $listingCatalog->listEventLocationOptions(120) : [];
$stmt = $pdo?->prepare( $stmt = $pdo?->prepare(
@@ -1000,6 +1004,7 @@ final class AccountPages
'Themen und Antworten moderieren', 'Themen und Antworten moderieren',
'Community-Sperren setzen und aufheben', 'Community-Sperren setzen und aufheben',
'Meldungen bearbeiten', 'Meldungen bearbeiten',
'Kategorien bestätigen und zusammenführen',
'Orte und Veranstaltungen freigeben', 'Orte und Veranstaltungen freigeben',
], ],
], ],
@@ -1009,7 +1014,6 @@ final class AccountPages
'rights' => [ 'rights' => [
'Beinhaltet alle Rechte von Forum-Admin', 'Beinhaltet alle Rechte von Forum-Admin',
'Community-Admin-Bewerbungen bearbeiten', 'Community-Admin-Bewerbungen bearbeiten',
'Kategorien bestätigen und zusammenführen',
'System-Einstellungen verwalten', 'System-Einstellungen verwalten',
], ],
], ],
@@ -1057,6 +1061,8 @@ final class AccountPages
'editListing', 'editListing',
'listingCategories', 'listingCategories',
'categoryReviewItems', 'categoryReviewItems',
'categorySearchQuery',
'categorySearchResults',
'placeSuggestions', 'placeSuggestions',
'eventLocationOptions', 'eventLocationOptions',
'communityPoints', 'communityPoints',

View File

@@ -77,7 +77,7 @@ final class CommunityAccess
public function canManageCategories(int $userId, ?float $points = null): bool 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; return true;
} }

View File

@@ -314,6 +314,68 @@ final class ListingCatalog
return $rows; 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 public function mergeCategories(string $sourceSlug, string $targetSlug): void
{ {
$this->ensureSchema(); $this->ensureSchema();