asdasd
All checks were successful
Deploy / deploy (push) Successful in 53s

This commit is contained in:
2026-08-17 00:49:49 +02:00
parent f7a6b34a97
commit c2eaa5c186
8 changed files with 86 additions and 37 deletions

View File

@@ -50,6 +50,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- 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`.
- 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.
## Technischer Rahmen ## Technischer Rahmen
- eigener Front-Controller in `public/index.php` - eigener Front-Controller in `public/index.php`

View File

@@ -24,6 +24,7 @@ Stand: 2026-08-10
- 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`
- eigener Mitgliederbereichspunkt `Kategorien` für neue Kategorien und deren Zusammenführung per Vorschlagsfeld: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php`
- Preis-, Bild- und Ortseingabe für Orte und Veranstaltungen sowie reduzierte Event-Maske: `partials/landing/account/dashboard.php` plus Speicherung in `src/App/ListingCatalog.php` - Preis-, Bild- und Ortseingabe für Orte und Veranstaltungen sowie reduzierte Event-Maske: `partials/landing/account/dashboard.php` plus Speicherung in `src/App/ListingCatalog.php`
- Freigaben sowie Änderungs- und Löschanfragen für Orte und Veranstaltungen: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php` - Freigaben sowie Änderungs- und Löschanfragen für Orte und Veranstaltungen: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php`
- Rollenvergabe per Benutzersuche im Community-Admin: `partials/landing/account/community-admin.php`, `src/App/CommunityAccess.php` - Rollenvergabe per Benutzersuche im Community-Admin: `partials/landing/account/community-admin.php`, `src/App/CommunityAccess.php`

View File

@@ -50,6 +50,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
- `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`.
- 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.
## Technical Frame ## Technical Frame
- custom front controller in `public/index.php` - custom front controller in `public/index.php`

View File

@@ -24,6 +24,7 @@ Updated: 2026-08-10
- 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`
- dedicated member-area section `Categories` for new categories and merging through a suggestion field: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php`
- pricing, image upload, and place/address input for places and event-style entries plus a reduced own-event form: `partials/landing/account/dashboard.php` with persistence in `src/App/ListingCatalog.php` - pricing, image upload, and place/address input for places and event-style entries plus a reduced own-event form: `partials/landing/account/dashboard.php` with persistence in `src/App/ListingCatalog.php`
- approvals as well as change and deletion requests for places and event-style entries: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php` - approvals as well as change and deletion requests for places and event-style entries: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php`
- role assignment via user search in community admin: `partials/landing/account/community-admin.php`, `src/App/CommunityAccess.php` - role assignment via user search in community admin: `partials/landing/account/community-admin.php`, `src/App/CommunityAccess.php`

View File

@@ -41,6 +41,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf:
- 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 - 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
- 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 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
- Im Mitgliederbereich ist `Events` jetzt wieder auf echte eigene Events und Event-Teilnahmen beschränkt - Im Mitgliederbereich ist `Events` jetzt wieder auf echte eigene Events und Event-Teilnahmen beschränkt

View File

@@ -148,6 +148,7 @@ if (!empty($canManageProfileLevels)) {
$showEventTypeChoice = !$editing && (($section ?? 'events') === 'places') && $requestedCreateListingKind === null; $showEventTypeChoice = !$editing && (($section ?? 'events') === 'places') && $requestedCreateListingKind === null;
$modalSectionTarget = ($editingListing || ($section ?? '') === 'places') ? 'places' : 'events'; $modalSectionTarget = ($editingListing || ($section ?? '') === 'places') ? 'places' : 'events';
if (!empty($canManageSystemSettings)) { if (!empty($canManageSystemSettings)) {
$sectionLinks['categories'] = 'Kategorien';
$sectionLinks['system'] = 'System'; $sectionLinks['system'] = 'System';
} }
?> ?>
@@ -946,44 +947,56 @@ if (!empty($canManageSystemSettings)) {
<li>Fehlende Tabellen: <?= !empty($listingCatalogStatus['missing']) ? htmlspecialchars(implode(', ', $listingCatalogStatus['missing']), ENT_QUOTES) : 'Keine' ?></li> <li>Fehlende Tabellen: <?= !empty($listingCatalogStatus['missing']) ? htmlspecialchars(implode(', ', $listingCatalogStatus['missing']), ENT_QUOTES) : 'Keine' ?></li>
</ul> </ul>
</div> </div>
</div>
</section>
<?php endif; ?>
<div class="card" style="margin-top:18px;"> <?php if ($section === 'categories' && !empty($canManageSystemSettings)): ?>
<strong>Kategorien prüfen und zusammenführen</strong> <section class="account-panel">
<p class="muted small" style="margin:10px 0 14px;">Neue Kategorien werden automatisch angelegt. Hier kannst du sie prüfen und bei Bedarf mit einer bestehenden Kategorie zusammenführen.</p> <div class="account-panel__head">
<?php if (!empty($categoryReviewItems)): ?> <h2>Kategorien</h2>
<div class="stack gap-10"> </div>
<?php foreach ($categoryReviewItems as $categoryItem): ?> <div class="account-panel__body">
<div class="card" style="padding:12px;"> <?php if (!empty($categoryReviewItems)): ?>
<div class="flex between center-y" style="gap:12px; flex-wrap:wrap;"> <datalist id="categoryMergeSuggestions">
<div> <?php foreach ($listingCategories as $targetCategory): ?>
<strong><?= htmlspecialchars((string)$categoryItem['title'], ENT_QUOTES) ?></strong> <option value="<?= htmlspecialchars((string)$targetCategory['title'], ENT_QUOTES) ?>"></option>
<?php if (!empty($categoryItem['needs_review'])): ?> <?php endforeach; ?>
<span class="badge" style="margin-left:8px;">Neu</span> </datalist>
<?php endif; ?> <div class="stack gap-10">
<div class="muted small" style="margin-top:6px;"> <?php foreach ($categoryReviewItems as $categoryItem): ?>
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 class="card" style="padding:12px;">
</div> <div>
</div> <strong><?= htmlspecialchars((string)$categoryItem['title'], ENT_QUOTES) ?></strong>
<span class="badge" style="margin-left:8px;">Neu</span>
<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="flex gap-8" style="margin-top:12px; flex-wrap:wrap;">
<input type="hidden" name="action" value="category_merge">
<input type="hidden" name="source_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
<select name="target_category_slug" class="select" style="min-width:260px;">
<option value="">Mit Kategorie zusammenführen</option>
<?php foreach ($listingCategories as $targetCategory): ?>
<?php if ((string)$targetCategory['slug'] === (string)$categoryItem['slug']) { continue; } ?>
<option value="<?= htmlspecialchars((string)$targetCategory['slug'], ENT_QUOTES) ?>"><?= htmlspecialchars((string)$targetCategory['title'], ENT_QUOTES) ?></option>
<?php endforeach; ?>
</select>
<button class="btn ghost" type="submit">Zusammenführen</button>
</form>
</div> </div>
<?php endforeach; ?> <form method="post" class="stack gap-8" style="margin-top:12px;">
</div> <input type="hidden" name="action" value="category_merge">
<?php else: ?> <input type="hidden" name="source_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
<p class="muted small" style="margin:12px 0 0;">Aktuell gibt es keine Kategorien zur Prüfung.</p> <div class="stack gap-6">
<?php endif; ?> <label class="label" for="targetCategoryInput<?= (int)($categoryItem['id'] ?? 0) ?>">Mit bestehender Kategorie zusammenführen</label>
</div> <input
id="targetCategoryInput<?= (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>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="muted small">Aktuell gibt es keine neuen Kategorien.</p>
<?php endif; ?>
</div> </div>
</section> </section>
<?php endif; ?> <?php endif; ?>

View File

@@ -183,6 +183,7 @@ final class AccountPages
$allowedSections = ['profile', 'children', 'events', 'places', 'community', 'settings']; $allowedSections = ['profile', 'children', 'events', 'places', 'community', 'settings'];
if ($canManageSystemSettings) { if ($canManageSystemSettings) {
$allowedSections[] = 'system'; $allowedSections[] = 'system';
$allowedSections[] = 'categories';
} }
if ($canManageProfileLevels) { if ($canManageProfileLevels) {
$allowedSections[] = 'profile-levels'; $allowedSections[] = 'profile-levels';
@@ -328,9 +329,13 @@ final class AccountPages
if (!$canManageSystemSettings || !$listingCatalog) { if (!$canManageSystemSettings || !$listingCatalog) {
throw new \RuntimeException('Keine Berechtigung für die Kategorien-Verwaltung.'); throw new \RuntimeException('Keine Berechtigung für die Kategorien-Verwaltung.');
} }
$targetCategorySlug = $listingCatalog->resolveExistingCategorySlug((string)($_POST['target_category_input'] ?? ''));
if ($targetCategorySlug === null) {
throw new \RuntimeException('Bitte wähle eine vorhandene Zielkategorie aus den Vorschlägen.');
}
$listingCatalog->mergeCategories( $listingCatalog->mergeCategories(
(string)($_POST['source_category_slug'] ?? ''), (string)($_POST['source_category_slug'] ?? ''),
(string)($_POST['target_category_slug'] ?? '') $targetCategorySlug
); );
$info = 'Kategorie zusammengeführt.'; $info = 'Kategorie zusammengeführt.';
} elseif ($action === 'child_add' || $action === 'child_update') { } elseif ($action === 'child_add' || $action === 'child_update') {
@@ -852,7 +857,10 @@ final class AccountPages
$restoreEventDraftActive = $section === 'events' && ((string)($_GET['restore_event_draft'] ?? '')) === '1' && self::hasStoredEventDraft(); $restoreEventDraftActive = $section === 'events' && ((string)($_GET['restore_event_draft'] ?? '')) === '1' && self::hasStoredEventDraft();
$restoredEventDraft = $restoreEventDraftActive ? self::getStoredEventDraft() : null; $restoredEventDraft = $restoreEventDraftActive ? self::getStoredEventDraft() : null;
$listingCategories = $listingCatalog ? $listingCatalog->listCategories(['general', 'place', 'food', 'event', 'family']) : []; $listingCategories = $listingCatalog ? $listingCatalog->listCategories(['general', 'place', 'food', 'event', 'family']) : [];
$categoryReviewItems = $listingCatalog && $canManageSystemSettings ? $listingCatalog->listCategoryReviewItems() : []; $categoryReviewItems = $listingCatalog && $canManageSystemSettings ? array_values(array_filter(
$listingCatalog->listCategoryReviewItems(),
static fn(array $item): bool => !empty($item['needs_review'])
)) : [];
$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(

View File

@@ -368,6 +368,29 @@ final class ListingCatalog
} }
} }
public function resolveExistingCategorySlug(string $input): ?string
{
$this->ensureSchema();
$input = trim($input);
if ($input === '') {
return null;
}
$stmt = $this->pdo->prepare(
'SELECT slug
FROM listing_categories
WHERE slug = :slug OR LOWER(title) = LOWER(:title)
ORDER BY sort_order ASC, id ASC
LIMIT 1'
);
$stmt->execute([
'slug' => $input,
'title' => $input,
]);
$slug = $stmt->fetchColumn();
return $slug !== false ? (string)$slug : null;
}
public function listPublishedDirectoryEntries(): array public function listPublishedDirectoryEntries(): array
{ {
$this->ensureSchema(); $this->ensureSchema();