This commit is contained in:
@@ -309,6 +309,15 @@ final class AccountPages
|
||||
'place_data_provider' => $placeDataProvider,
|
||||
], $userId);
|
||||
$info = 'System-Einstellungen gespeichert.';
|
||||
} elseif ($action === 'category_merge') {
|
||||
if (!$canManageSystemSettings || !$listingCatalog) {
|
||||
throw new \RuntimeException('Keine Berechtigung für die Kategorien-Verwaltung.');
|
||||
}
|
||||
$listingCatalog->mergeCategories(
|
||||
(string)($_POST['source_category_slug'] ?? ''),
|
||||
(string)($_POST['target_category_slug'] ?? '')
|
||||
);
|
||||
$info = 'Kategorie zusammengeführt.';
|
||||
} elseif ($action === 'child_add' || $action === 'child_update') {
|
||||
$crypto = self::requireCrypto($crypto, 'Kinder');
|
||||
$childId = (int)($_POST['child_id'] ?? 0);
|
||||
@@ -459,12 +468,15 @@ final class AccountPages
|
||||
$imagePath = self::storeUploadedImage($_FILES['image_file'] ?? null, is_array($existingEvent) ? (string)($existingEvent['image_path'] ?? '') : '');
|
||||
$allowKids = ((string)($_POST['with_children'] ?? 'yes')) === 'yes' ? 1 : 0;
|
||||
$capacity = isset($_POST['max_participants']) && $_POST['max_participants'] !== '' ? (int)$_POST['max_participants'] : null;
|
||||
$categorySlug = trim((string)($_POST['category_slug'] ?? ''));
|
||||
$customCategoryTitle = trim((string)($_POST['category_custom_title'] ?? ''));
|
||||
if ($customCategoryTitle !== '') {
|
||||
$categorySlug = self::slugifyValue($customCategoryTitle);
|
||||
$categoryInput = trim((string)($_POST['category_input'] ?? $_POST['category_slug'] ?? ''));
|
||||
$categorySlug = '';
|
||||
if ($categoryInput !== '' && $listingCatalog) {
|
||||
$category = $listingCatalog->ensureCategoryForInput($categoryInput, 'event');
|
||||
$categorySlug = (string)($category['slug'] ?? '');
|
||||
}
|
||||
|
||||
$eventStartValue = self::normalizeDateForStorage((string)($_POST['starts_at'] ?? ''), true);
|
||||
|
||||
if ($action === 'event_add') {
|
||||
$stmt = $pdo?->prepare('INSERT INTO events (created_by, title, teaser_public, description, category_slug, image_path, location_label, street, zip, city, region, lat, lng, starts_at, max_participants, allow_kids, visibility, status, created_at, updated_at) VALUES (:uid, :title, :teaser, :descr, :categorySlug, :imagePath, :loc, :street, :zip, :city, :region, :lat, :lng, :start, :capacity, :allow, :vis, :status, NOW(), NOW())');
|
||||
$stmt?->execute([
|
||||
@@ -474,14 +486,14 @@ final class AccountPages
|
||||
'descr' => trim((string)$_POST['description']),
|
||||
'categorySlug' => $categorySlug !== '' ? $categorySlug : null,
|
||||
'imagePath' => $imagePath !== '' ? $imagePath : null,
|
||||
'loc' => trim((string)$_POST['location_label']),
|
||||
'loc' => null,
|
||||
'street' => $street ?: null,
|
||||
'zip' => $zip,
|
||||
'city' => $city,
|
||||
'region' => $region,
|
||||
'lat' => $lat,
|
||||
'lng' => $lng,
|
||||
'start' => $_POST['starts_at'] ?? null,
|
||||
'start' => $eventStartValue,
|
||||
'capacity' => $capacity,
|
||||
'allow' => $allowKids,
|
||||
'vis' => $_POST['visibility'] ?? 'public',
|
||||
@@ -506,14 +518,14 @@ final class AccountPages
|
||||
'descr' => trim((string)$_POST['description']),
|
||||
'categorySlug' => $categorySlug !== '' ? $categorySlug : null,
|
||||
'imagePath' => $imagePath !== '' ? $imagePath : null,
|
||||
'loc' => trim((string)$_POST['location_label']),
|
||||
'loc' => null,
|
||||
'street' => $street ?: null,
|
||||
'zip' => $zip,
|
||||
'city' => $city,
|
||||
'region' => $region,
|
||||
'lat' => $lat,
|
||||
'lng' => $lng,
|
||||
'start' => $_POST['starts_at'] ?? null,
|
||||
'start' => $eventStartValue,
|
||||
'capacity' => $capacity,
|
||||
'allow' => $allowKids,
|
||||
'vis' => $_POST['visibility'] ?? 'public',
|
||||
@@ -682,7 +694,8 @@ final class AccountPages
|
||||
$editEvent = null;
|
||||
$otherListings = [];
|
||||
$editListing = null;
|
||||
$listingCategories = $listingCatalog ? $listingCatalog->listCategories(['place', 'food', 'event', 'family']) : [];
|
||||
$listingCategories = $listingCatalog ? $listingCatalog->listCategories(['general', 'place', 'food', 'event', 'family']) : [];
|
||||
$categoryReviewItems = $listingCatalog && $canManageSystemSettings ? $listingCatalog->listCategoryReviewItems() : [];
|
||||
$placeSuggestions = $listingCatalog ? $listingCatalog->listPlaceSuggestions(60) : [];
|
||||
$stmt = $pdo?->prepare(
|
||||
'SELECT e.id, e.title, e.teaser_public, e.description, e.category_slug, e.image_path, e.location_label, e.street, e.zip, e.city, e.region, e.starts_at, e.max_participants, e.allow_kids, e.visibility, e.status, e.lat, e.lng,
|
||||
@@ -750,6 +763,7 @@ final class AccountPages
|
||||
'otherListings',
|
||||
'editListing',
|
||||
'listingCategories',
|
||||
'categoryReviewItems',
|
||||
'placeSuggestions',
|
||||
'communityPoints',
|
||||
'communityLevel',
|
||||
@@ -973,6 +987,20 @@ final class AccountPages
|
||||
return trim($value, '-');
|
||||
}
|
||||
|
||||
private static function normalizeDateForStorage(string $value, bool $endOfDay = false): ?string
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1) {
|
||||
return $value . ($endOfDay ? ' 23:59:59' : ' 00:00:00');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function reverseGeocodeAddress(float $lat, float $lng): ?array
|
||||
{
|
||||
$url = 'https://nominatim.openstreetmap.org/reverse?' . http_build_query([
|
||||
|
||||
@@ -217,6 +217,136 @@ final class ListingCatalog
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function ensureCategoryForInput(string $input, string $group = 'general'): ?array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$input = trim($input);
|
||||
if ($input === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slug = $this->slugify($input);
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT id, slug, title, category_group, sort_order
|
||||
FROM listing_categories
|
||||
WHERE slug = :slug OR LOWER(title) = LOWER(:title)
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([
|
||||
'slug' => $slug,
|
||||
'title' => $input,
|
||||
]);
|
||||
$existing = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if ($existing) {
|
||||
$existing['is_new'] = false;
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$insert = $this->pdo->prepare(
|
||||
'INSERT INTO listing_categories (slug, title, category_group, sort_order)
|
||||
VALUES (:slug, :title, :groupName, 999)'
|
||||
);
|
||||
$insert->execute([
|
||||
'slug' => $slug,
|
||||
'title' => $input,
|
||||
'groupName' => in_array($group, ['general', 'event', 'place', 'food', 'family', 'partner'], true) ? $group : 'general',
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => (int)$this->pdo->lastInsertId(),
|
||||
'slug' => $slug,
|
||||
'title' => $input,
|
||||
'category_group' => $group,
|
||||
'sort_order' => 999,
|
||||
'is_new' => true,
|
||||
];
|
||||
}
|
||||
|
||||
public function listCategoryReviewItems(): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->query(
|
||||
'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
|
||||
ORDER BY
|
||||
CASE WHEN c.sort_order >= 900 THEN 0 ELSE 1 END,
|
||||
c.sort_order ASC,
|
||||
c.title ASC'
|
||||
);
|
||||
$rows = $stmt ? ($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();
|
||||
$sourceSlug = trim($sourceSlug);
|
||||
$targetSlug = trim($targetSlug);
|
||||
if ($sourceSlug === '' || $targetSlug === '' || $sourceSlug === $targetSlug) {
|
||||
throw new \RuntimeException('Bitte zwei unterschiedliche Kategorien auswählen.');
|
||||
}
|
||||
|
||||
$lookup = $this->pdo->prepare('SELECT id, slug FROM listing_categories WHERE slug = :slug LIMIT 1');
|
||||
$lookup->execute(['slug' => $sourceSlug]);
|
||||
$source = $lookup->fetch(\PDO::FETCH_ASSOC);
|
||||
$lookup->execute(['slug' => $targetSlug]);
|
||||
$target = $lookup->fetch(\PDO::FETCH_ASSOC);
|
||||
if (!$source || !$target) {
|
||||
throw new \RuntimeException('Kategorie nicht gefunden.');
|
||||
}
|
||||
|
||||
$sourceId = (int)$source['id'];
|
||||
$targetId = (int)$target['id'];
|
||||
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
$mapRows = $this->pdo->prepare('SELECT listing_id FROM listing_category_map WHERE category_id = :categoryId');
|
||||
$mapRows->execute(['categoryId' => $sourceId]);
|
||||
$listingIds = $mapRows->fetchAll(\PDO::FETCH_COLUMN) ?: [];
|
||||
$insertMap = $this->pdo->prepare(
|
||||
'INSERT IGNORE INTO listing_category_map (listing_id, category_id) VALUES (:listingId, :categoryId)'
|
||||
);
|
||||
foreach ($listingIds as $listingId) {
|
||||
$insertMap->execute([
|
||||
'listingId' => (int)$listingId,
|
||||
'categoryId' => $targetId,
|
||||
]);
|
||||
}
|
||||
$this->pdo->prepare('DELETE FROM listing_category_map WHERE category_id = :categoryId')->execute(['categoryId' => $sourceId]);
|
||||
$this->pdo->prepare('UPDATE listing_places SET place_kind = :target WHERE place_kind = :source')->execute([
|
||||
'target' => $targetSlug,
|
||||
'source' => $sourceSlug,
|
||||
]);
|
||||
$this->pdo->prepare('UPDATE events SET category_slug = :target WHERE category_slug = :source')->execute([
|
||||
'target' => $targetSlug,
|
||||
'source' => $sourceSlug,
|
||||
]);
|
||||
$this->pdo->prepare('DELETE FROM listing_categories WHERE id = :id')->execute(['id' => $sourceId]);
|
||||
$this->pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function listDashboardEntries(int $userId): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
@@ -302,8 +432,8 @@ final class ListingCatalog
|
||||
$openingHoursNote = trim((string)($data['opening_hours_note'] ?? ''));
|
||||
$openingHoursRows = $this->normalizeOpeningHoursRows($data);
|
||||
$visibility = (string)($data['visibility'] ?? 'public');
|
||||
$placeKind = trim((string)($data['category_slug'] ?? ''));
|
||||
$customCategoryTitle = trim((string)($data['category_custom_title'] ?? ''));
|
||||
$categoryInput = trim((string)($data['category_input'] ?? $data['category_slug'] ?? ''));
|
||||
$placeKind = $categoryInput;
|
||||
$street = trim((string)($data['street'] ?? ''));
|
||||
$zip = trim((string)($data['zip'] ?? ''));
|
||||
$city = trim((string)($data['city'] ?? ''));
|
||||
@@ -332,10 +462,14 @@ final class ListingCatalog
|
||||
if ($entryType === 'editorial_event' && $recurrenceUntil === '') {
|
||||
throw new \RuntimeException('Bitte gib für Veranstaltungen ein Gültig-bis-Datum an.');
|
||||
}
|
||||
$startsAt = $this->normalizeDateForStorage($startsAt, false);
|
||||
$recurrenceUntil = $this->normalizeDateForStorage($recurrenceUntil, true);
|
||||
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
[$placeKind, $categoryId] = $this->resolveCategory($placeKind, $customCategoryTitle);
|
||||
$category = $this->ensureCategoryForInput($categoryInput, $entryType === 'place' ? 'place' : 'event');
|
||||
$placeKind = (string)($category['slug'] ?? '');
|
||||
$categoryId = isset($category['id']) ? (int)$category['id'] : null;
|
||||
|
||||
if ($listingId !== null && $listingId > 0) {
|
||||
$existing = $this->getDashboardEntry($userId, $listingId);
|
||||
@@ -536,31 +670,6 @@ final class ListingCatalog
|
||||
return ['single', null];
|
||||
}
|
||||
|
||||
private function resolveCategory(string $slug, string $customTitle): array
|
||||
{
|
||||
$slug = trim($slug);
|
||||
$customTitle = trim($customTitle);
|
||||
if ($customTitle !== '') {
|
||||
$slug = $this->slugify($customTitle);
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO listing_categories (slug, title, category_group, sort_order)
|
||||
VALUES (:slug, :title, "general", 999)
|
||||
ON DUPLICATE KEY UPDATE title = VALUES(title), updated_at = CURRENT_TIMESTAMP'
|
||||
);
|
||||
$stmt->execute([
|
||||
'slug' => $slug,
|
||||
'title' => $customTitle,
|
||||
]);
|
||||
}
|
||||
if ($slug === '') {
|
||||
return ['', null];
|
||||
}
|
||||
$stmt = $this->pdo->prepare('SELECT id FROM listing_categories WHERE slug = :slug LIMIT 1');
|
||||
$stmt->execute(['slug' => $slug]);
|
||||
$categoryId = $stmt->fetchColumn();
|
||||
return [$slug, $categoryId !== false ? (int)$categoryId : null];
|
||||
}
|
||||
|
||||
private function normalizeOpeningHoursRows(array $data): array
|
||||
{
|
||||
$labels = (array)($data['opening_day_label'] ?? []);
|
||||
@@ -586,32 +695,19 @@ final class ListingCatalog
|
||||
|
||||
private function normalizePriceRows(array $data): array
|
||||
{
|
||||
$labels = (array)($data['price_label'] ?? []);
|
||||
$audiences = (array)($data['price_audience'] ?? []);
|
||||
$types = (array)($data['price_type'] ?? []);
|
||||
$amounts = (array)($data['price_amount'] ?? []);
|
||||
$secondaryAmounts = (array)($data['price_amount_secondary'] ?? []);
|
||||
$ageFroms = (array)($data['price_age_from'] ?? []);
|
||||
$ageTos = (array)($data['price_age_to'] ?? []);
|
||||
$notes = (array)($data['price_note'] ?? []);
|
||||
$count = max(count($labels), count($audiences), count($types), count($amounts), count($secondaryAmounts), count($ageFroms), count($ageTos), count($notes));
|
||||
$count = max(count($amounts), count($ageFroms), count($ageTos));
|
||||
$rows = [];
|
||||
for ($index = 0; $index < $count; $index++) {
|
||||
$label = trim((string)($labels[$index] ?? ''));
|
||||
$audience = trim((string)($audiences[$index] ?? 'general'));
|
||||
$priceType = trim((string)($types[$index] ?? 'fixed'));
|
||||
$amount = trim((string)($amounts[$index] ?? ''));
|
||||
$secondary = trim((string)($secondaryAmounts[$index] ?? ''));
|
||||
$ageFrom = trim((string)($ageFroms[$index] ?? ''));
|
||||
$ageTo = trim((string)($ageTos[$index] ?? ''));
|
||||
$note = trim((string)($notes[$index] ?? ''));
|
||||
if ($label === '' && $amount === '' && $secondary === '' && $note === '' && $ageFrom === '' && $ageTo === '') {
|
||||
if ($amount === '' && $ageFrom === '' && $ageTo === '') {
|
||||
continue;
|
||||
}
|
||||
$noteParts = [];
|
||||
if ($note !== '') {
|
||||
$noteParts[] = $note;
|
||||
}
|
||||
if ($ageFrom !== '' || $ageTo !== '') {
|
||||
$ageText = [];
|
||||
if ($ageFrom !== '') {
|
||||
@@ -623,11 +719,11 @@ final class ListingCatalog
|
||||
$noteParts[] = implode(', ', $ageText);
|
||||
}
|
||||
$rows[] = [
|
||||
'label' => $label !== '' ? $label : 'Preis',
|
||||
'audience' => in_array($audience, ['general', 'adult', 'child', 'family', 'group'], true) ? $audience : 'general',
|
||||
'price_type' => in_array($priceType, ['free', 'fixed', 'from', 'up_to', 'range', 'request'], true) ? $priceType : 'fixed',
|
||||
'label' => 'Preis',
|
||||
'audience' => 'general',
|
||||
'price_type' => $amount === '' ? 'request' : 'fixed',
|
||||
'amount' => $amount !== '' ? (float)str_replace(',', '.', $amount) : null,
|
||||
'amount_secondary' => $secondary !== '' ? (float)str_replace(',', '.', $secondary) : null,
|
||||
'amount_secondary' => null,
|
||||
'note' => $noteParts !== [] ? implode(' | ', $noteParts) : null,
|
||||
];
|
||||
}
|
||||
@@ -648,6 +744,18 @@ final class ListingCatalog
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
private function normalizeDateForStorage(string $value, bool $endOfDay): string
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1) {
|
||||
return $value . ($endOfDay ? ' 23:59:59' : ' 00:00:00');
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function slugify(string $value): string
|
||||
{
|
||||
$value = mb_strtolower(trim($value));
|
||||
|
||||
Reference in New Issue
Block a user