This commit is contained in:
@@ -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`
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; ?>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user