This commit is contained in:
@@ -141,6 +141,26 @@ final class ListingCatalog
|
||||
CONSTRAINT fk_listing_benefits_occurrence FOREIGN KEY (occurrence_id) REFERENCES listing_occurrences(id) ON DELETE CASCADE,
|
||||
INDEX idx_listing_benefits_listing (listing_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
'CREATE TABLE IF NOT EXISTS listing_moderation_requests (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
listing_id BIGINT UNSIGNED NOT NULL,
|
||||
request_type ENUM("create","update","delete") NOT NULL,
|
||||
request_status ENUM("open","approved","rejected") NOT NULL DEFAULT "open",
|
||||
requested_by BIGINT UNSIGNED NOT NULL,
|
||||
reviewed_by BIGINT UNSIGNED NULL,
|
||||
request_reason TEXT NULL,
|
||||
review_note TEXT NULL,
|
||||
payload_json LONGTEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
reviewed_at DATETIME NULL,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_listing_moderation_listing FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_listing_moderation_requested_by FOREIGN KEY (requested_by) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_listing_moderation_reviewed_by FOREIGN KEY (reviewed_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
INDEX idx_listing_moderation_status (request_status),
|
||||
INDEX idx_listing_moderation_type (request_type),
|
||||
INDEX idx_listing_moderation_user (requested_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
];
|
||||
|
||||
foreach ($statements as $sql) {
|
||||
@@ -186,6 +206,7 @@ final class ListingCatalog
|
||||
'listing_occurrences',
|
||||
'listing_prices',
|
||||
'listing_benefits',
|
||||
'listing_moderation_requests',
|
||||
];
|
||||
$missing = [];
|
||||
foreach ($tables as $table) {
|
||||
@@ -347,12 +368,12 @@ final class ListingCatalog
|
||||
}
|
||||
}
|
||||
|
||||
public function listDashboardEntries(int $userId): array
|
||||
public function listPublishedDirectoryEntries(): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT l.id, l.listing_type, l.title, l.teaser_public, l.visibility, l.status, l.image_path,
|
||||
lp.title AS place_title, lp.city, lp.region, lp.place_kind,
|
||||
$stmt = $this->pdo->query(
|
||||
'SELECT l.id, l.created_by, l.listing_type, l.title, l.teaser_public, l.visibility, l.status, l.image_path,
|
||||
lp.title AS place_title, lp.street, lp.zip, lp.city, lp.region, lp.place_kind,
|
||||
lo.starts_at, lo.ends_at, lo.occurrence_type,
|
||||
lc.title AS category_title, lc.slug AS category_slug
|
||||
FROM listings l
|
||||
@@ -360,6 +381,46 @@ final class ListingCatalog
|
||||
LEFT JOIN listing_occurrences lo ON lo.listing_id = l.id
|
||||
LEFT JOIN listing_category_map lcm ON lcm.listing_id = l.id
|
||||
LEFT JOIN listing_categories lc ON lc.id = lcm.category_id
|
||||
WHERE l.listing_type IN ("place","editorial_event")
|
||||
AND l.status = "published"
|
||||
AND (lp.status = "published" OR lp.status IS NULL)
|
||||
ORDER BY
|
||||
CASE l.listing_type
|
||||
WHEN "place" THEN 1
|
||||
WHEN "editorial_event" THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
l.title ASC,
|
||||
COALESCE(lo.starts_at, l.created_at) ASC'
|
||||
);
|
||||
return $stmt ? ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) : [];
|
||||
}
|
||||
|
||||
public function listUserSubmittedEntries(int $userId): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT l.id, l.listing_type, l.title, l.teaser_public, l.visibility, l.status, l.image_path,
|
||||
lp.title AS place_title, lp.city, lp.region, lp.place_kind,
|
||||
lo.starts_at, lo.ends_at, lo.occurrence_type,
|
||||
lc.title AS category_title, lc.slug AS category_slug,
|
||||
mr.request_type AS moderation_request_type,
|
||||
mr.request_status AS moderation_request_status,
|
||||
mr.review_note AS moderation_review_note,
|
||||
mr.created_at AS moderation_created_at
|
||||
FROM listings l
|
||||
LEFT JOIN listing_places lp ON lp.id = l.primary_place_id
|
||||
LEFT JOIN listing_occurrences lo ON lo.listing_id = l.id
|
||||
LEFT JOIN listing_category_map lcm ON lcm.listing_id = l.id
|
||||
LEFT JOIN listing_categories lc ON lc.id = lcm.category_id
|
||||
LEFT JOIN listing_moderation_requests mr
|
||||
ON mr.id = (
|
||||
SELECT sub.id
|
||||
FROM listing_moderation_requests sub
|
||||
WHERE sub.listing_id = l.id
|
||||
ORDER BY sub.created_at DESC, sub.id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE l.created_by = :uid
|
||||
ORDER BY
|
||||
CASE l.listing_type
|
||||
@@ -374,7 +435,12 @@ final class ListingCatalog
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function getDashboardEntry(int $userId, int $listingId): ?array
|
||||
public function listDashboardEntries(int $userId): array
|
||||
{
|
||||
return $this->listUserSubmittedEntries($userId);
|
||||
}
|
||||
|
||||
public function getMemberEntry(int $listingId): ?array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->prepare(
|
||||
@@ -387,13 +453,10 @@ final class ListingCatalog
|
||||
LEFT JOIN listing_occurrences lo ON lo.listing_id = l.id
|
||||
LEFT JOIN listing_category_map lcm ON lcm.listing_id = l.id
|
||||
LEFT JOIN listing_categories lc ON lc.id = lcm.category_id
|
||||
WHERE l.id = :id AND l.created_by = :uid
|
||||
WHERE l.id = :id
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([
|
||||
'id' => $listingId,
|
||||
'uid' => $userId,
|
||||
]);
|
||||
$stmt->execute(['id' => $listingId]);
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if (!$row) {
|
||||
return null;
|
||||
@@ -403,14 +466,26 @@ final class ListingCatalog
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function getDashboardEntry(int $userId, int $listingId): ?array
|
||||
{
|
||||
$row = $this->getMemberEntry($listingId);
|
||||
if (!$row || (int)($row['created_by'] ?? 0) !== $userId) {
|
||||
return null;
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function listPlaceSuggestions(int $limit = 40): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT id, title, street, zip, city, region, place_kind
|
||||
FROM listing_places
|
||||
WHERE status = "published"
|
||||
ORDER BY updated_at DESC
|
||||
'SELECT DISTINCT lp.id, lp.title, lp.street, lp.zip, lp.city, lp.region, lp.place_kind
|
||||
FROM listing_places lp
|
||||
INNER JOIN listings l ON l.primary_place_id = lp.id
|
||||
WHERE lp.status IN ("draft","published")
|
||||
AND l.status IN ("draft","published")
|
||||
AND l.listing_type IN ("place","editorial_event")
|
||||
ORDER BY lp.updated_at DESC
|
||||
LIMIT :limit'
|
||||
);
|
||||
$stmt->bindValue(':limit', max(1, min(200, $limit)), \PDO::PARAM_INT);
|
||||
@@ -418,7 +493,170 @@ final class ListingCatalog
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function saveDashboardEntry(int $userId, array $data, ?int $listingId = null): int
|
||||
public function listUserOpenModerationRequestMap(int $userId): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT listing_id, GROUP_CONCAT(request_type ORDER BY created_at ASC SEPARATOR ",") AS request_types
|
||||
FROM listing_moderation_requests
|
||||
WHERE requested_by = :uid
|
||||
AND request_status = "open"
|
||||
GROUP BY listing_id'
|
||||
);
|
||||
$stmt->execute(['uid' => $userId]);
|
||||
$map = [];
|
||||
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) {
|
||||
$types = array_values(array_filter(array_map('trim', explode(',', (string)($row['request_types'] ?? '')))));
|
||||
$map[(int)$row['listing_id']] = $types;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function submitCreateSuggestion(int $userId, array $data): int
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$this->assertNoDuplicateEntry($data);
|
||||
$listingId = $this->saveDashboardEntry($userId, $data, null, 'draft', 'draft');
|
||||
$this->createModerationRequest($listingId, 'create', $userId, null, null);
|
||||
return $listingId;
|
||||
}
|
||||
|
||||
public function submitUpdateRequest(int $userId, int $listingId, array $data, string $reason): int
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$reason = trim($reason);
|
||||
if ($reason === '') {
|
||||
throw new \RuntimeException('Bitte gib eine Begründung für die Änderungsanfrage an.');
|
||||
}
|
||||
$entry = $this->getMemberEntry($listingId);
|
||||
if (!$entry || (string)($entry['status'] ?? '') !== 'published') {
|
||||
throw new \RuntimeException('Eintrag nicht gefunden.');
|
||||
}
|
||||
if ($this->hasOpenModerationRequest($listingId, ['update'])) {
|
||||
throw new \RuntimeException('Für diesen Eintrag gibt es bereits eine offene Änderungsanfrage.');
|
||||
}
|
||||
|
||||
$proposal = $this->buildModerationPayload($data, $entry);
|
||||
$this->assertNoDuplicateEntry($proposal, $listingId);
|
||||
return $this->createModerationRequest($listingId, 'update', $userId, $reason, $proposal);
|
||||
}
|
||||
|
||||
public function submitDeleteRequest(int $userId, int $listingId, string $reason): int
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$reason = trim($reason);
|
||||
if ($reason === '') {
|
||||
throw new \RuntimeException('Bitte gib eine Begründung für die Löschanfrage an.');
|
||||
}
|
||||
$entry = $this->getMemberEntry($listingId);
|
||||
if (!$entry || (string)($entry['status'] ?? '') !== 'published') {
|
||||
throw new \RuntimeException('Eintrag nicht gefunden.');
|
||||
}
|
||||
if ($this->hasOpenModerationRequest($listingId, ['delete'])) {
|
||||
throw new \RuntimeException('Für diesen Eintrag gibt es bereits eine offene Löschanfrage.');
|
||||
}
|
||||
|
||||
return $this->createModerationRequest($listingId, 'delete', $userId, $reason, null);
|
||||
}
|
||||
|
||||
public function listOpenModerationRequests(): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->query(
|
||||
'SELECT mr.*, l.title, l.listing_type, l.status AS listing_status,
|
||||
lp.street, lp.zip, lp.city, lp.region,
|
||||
up.display_name AS requested_by_name
|
||||
FROM listing_moderation_requests mr
|
||||
INNER JOIN listings l ON l.id = mr.listing_id
|
||||
LEFT JOIN listing_places lp ON lp.id = l.primary_place_id
|
||||
LEFT JOIN user_profiles up ON up.user_id = mr.requested_by
|
||||
WHERE mr.request_status = "open"
|
||||
ORDER BY mr.created_at ASC, mr.id ASC'
|
||||
);
|
||||
$rows = $stmt ? ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) : [];
|
||||
foreach ($rows as &$row) {
|
||||
$row['payload'] = $this->decodeJsonRows((string)($row['payload_json'] ?? ''));
|
||||
}
|
||||
unset($row);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function decideModerationRequest(int $reviewerUserId, int $requestId, string $decision, ?string $reviewNote = null): void
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$decision = $decision === 'approved' ? 'approved' : 'rejected';
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT mr.*, l.created_by, l.primary_place_id, l.image_path, l.listing_type, l.status AS listing_status
|
||||
FROM listing_moderation_requests mr
|
||||
INNER JOIN listings l ON l.id = mr.listing_id
|
||||
WHERE mr.id = :id
|
||||
AND mr.request_status = "open"
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute(['id' => $requestId]);
|
||||
$request = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if (!$request) {
|
||||
throw new \RuntimeException('Moderationsanfrage nicht gefunden.');
|
||||
}
|
||||
|
||||
$payload = $this->decodeJsonRows((string)($request['payload_json'] ?? ''));
|
||||
$listingId = (int)$request['listing_id'];
|
||||
$createdBy = (int)($request['created_by'] ?? 0);
|
||||
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
if ($decision === 'approved') {
|
||||
if ((string)$request['request_type'] === 'create') {
|
||||
$currentEntry = $this->getMemberEntry($listingId);
|
||||
if (!$currentEntry) {
|
||||
throw new \RuntimeException('Eintrag nicht gefunden.');
|
||||
}
|
||||
$this->assertNoDuplicateEntry($currentEntry, $listingId);
|
||||
$this->pdo->prepare('UPDATE listings SET status = "published", updated_at = NOW() WHERE id = :id')->execute(['id' => $listingId]);
|
||||
if (!empty($request['primary_place_id'])) {
|
||||
$this->pdo->prepare('UPDATE listing_places SET status = "published", updated_at = NOW() WHERE id = :id')->execute(['id' => (int)$request['primary_place_id']]);
|
||||
}
|
||||
} elseif ((string)$request['request_type'] === 'update') {
|
||||
if ($payload === []) {
|
||||
throw new \RuntimeException('Für diese Änderungsanfrage fehlen die vorgeschlagenen Daten.');
|
||||
}
|
||||
$this->assertNoDuplicateEntry($payload, $listingId);
|
||||
$this->saveDashboardEntry($createdBy, $payload, $listingId, 'published', 'published');
|
||||
} elseif ((string)$request['request_type'] === 'delete') {
|
||||
$this->pdo->prepare('UPDATE listings SET status = "archived", updated_at = NOW() WHERE id = :id')->execute(['id' => $listingId]);
|
||||
if (!empty($request['primary_place_id'])) {
|
||||
$this->pdo->prepare('UPDATE listing_places SET status = "archived", updated_at = NOW() WHERE id = :id')->execute(['id' => (int)$request['primary_place_id']]);
|
||||
}
|
||||
}
|
||||
} elseif ((string)$request['request_type'] === 'create') {
|
||||
$this->pdo->prepare('UPDATE listings SET status = "archived", updated_at = NOW() WHERE id = :id')->execute(['id' => $listingId]);
|
||||
if (!empty($request['primary_place_id'])) {
|
||||
$this->pdo->prepare('UPDATE listing_places SET status = "archived", updated_at = NOW() WHERE id = :id')->execute(['id' => (int)$request['primary_place_id']]);
|
||||
}
|
||||
}
|
||||
|
||||
$update = $this->pdo->prepare(
|
||||
'UPDATE listing_moderation_requests
|
||||
SET request_status = :status, reviewed_by = :reviewedBy, review_note = :reviewNote, reviewed_at = NOW(), updated_at = NOW()
|
||||
WHERE id = :id'
|
||||
);
|
||||
$update->execute([
|
||||
'status' => $decision,
|
||||
'reviewedBy' => $reviewerUserId,
|
||||
'reviewNote' => trim((string)$reviewNote) !== '' ? trim((string)$reviewNote) : null,
|
||||
'id' => $requestId,
|
||||
]);
|
||||
|
||||
$this->pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function saveDashboardEntry(int $userId, array $data, ?int $listingId = null, string $listingStatus = 'published', string $placeStatus = 'published'): int
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$entryType = (string)($data['entry_kind'] ?? 'editorial_event');
|
||||
@@ -462,10 +700,19 @@ final class ListingCatalog
|
||||
if ($entryType === 'editorial_event' && $recurrenceUntil === '') {
|
||||
throw new \RuntimeException('Bitte gib für Veranstaltungen ein Gültig-bis-Datum an.');
|
||||
}
|
||||
if (!in_array($listingStatus, ['draft', 'published', 'cancelled', 'archived'], true)) {
|
||||
$listingStatus = 'draft';
|
||||
}
|
||||
if (!in_array($placeStatus, ['draft', 'published', 'archived'], true)) {
|
||||
$placeStatus = 'draft';
|
||||
}
|
||||
$startsAt = $this->normalizeDateForStorage($startsAt, false);
|
||||
$recurrenceUntil = $this->normalizeDateForStorage($recurrenceUntil, true);
|
||||
|
||||
$this->pdo->beginTransaction();
|
||||
$managesTransaction = !$this->pdo->inTransaction();
|
||||
if ($managesTransaction) {
|
||||
$this->pdo->beginTransaction();
|
||||
}
|
||||
try {
|
||||
$category = $this->ensureCategoryForInput($categoryInput, $entryType === 'place' ? 'place' : 'event');
|
||||
$placeKind = (string)($category['slug'] ?? '');
|
||||
@@ -482,7 +729,7 @@ final class ListingCatalog
|
||||
'UPDATE listing_places
|
||||
SET title = :title, description = :description, street = :street, zip = :zip, city = :city, region = :region,
|
||||
lat = :lat, lng = :lng, website_url = :websiteUrl, phone = :phone, place_kind = :placeKind,
|
||||
opening_hours_note = :openingHoursNote, opening_hours_json = :openingHoursJson, updated_at = NOW()
|
||||
opening_hours_note = :openingHoursNote, opening_hours_json = :openingHoursJson, status = :placeStatus, updated_at = NOW()
|
||||
WHERE id = :id'
|
||||
);
|
||||
$placeStmt->execute([
|
||||
@@ -499,6 +746,7 @@ final class ListingCatalog
|
||||
'placeKind' => $placeKind !== '' ? $placeKind : null,
|
||||
'openingHoursNote' => $openingHoursNote !== '' ? $openingHoursNote : null,
|
||||
'openingHoursJson' => $openingHoursRows !== [] ? json_encode($openingHoursRows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
|
||||
'placeStatus' => $placeStatus,
|
||||
'id' => $placeId,
|
||||
]);
|
||||
}
|
||||
@@ -507,7 +755,7 @@ final class ListingCatalog
|
||||
SET listing_type = :listingType, title = :title, teaser_public = :teaser, description = :description,
|
||||
image_path = :imagePath, special_conditions_note = :specialConditionsNote, visibility = :visibility,
|
||||
supports_registration = 0, supports_capacity = :supportsCapacity, supports_pricing = :supportsPricing,
|
||||
is_recurring = :isRecurring, status = "published", updated_at = NOW()
|
||||
is_recurring = :isRecurring, status = :listingStatus, updated_at = NOW()
|
||||
WHERE id = :id AND created_by = :uid'
|
||||
);
|
||||
$listingStmt->execute([
|
||||
@@ -521,6 +769,7 @@ final class ListingCatalog
|
||||
'supportsCapacity' => $capacityTotal !== null ? 1 : 0,
|
||||
'supportsPricing' => $this->hasPriceRows($data) || $specialConditionsNote !== '' ? 1 : 0,
|
||||
'isRecurring' => $entryType === 'editorial_event' && $recurrenceMode !== 'single' ? 1 : 0,
|
||||
'listingStatus' => $listingStatus,
|
||||
'id' => $listingId,
|
||||
'uid' => $userId,
|
||||
]);
|
||||
@@ -530,7 +779,7 @@ final class ListingCatalog
|
||||
} else {
|
||||
$placeStmt = $this->pdo->prepare(
|
||||
'INSERT INTO listing_places (created_by, source_type, title, description, street, zip, city, region, lat, lng, website_url, phone, place_kind, opening_hours_note, opening_hours_json, provider_hint, status, created_at, updated_at)
|
||||
VALUES (:uid, "user", :title, :description, :street, :zip, :city, :region, :lat, :lng, :websiteUrl, :phone, :placeKind, :openingHoursNote, :openingHoursJson, "manual", "published", NOW(), NOW())'
|
||||
VALUES (:uid, "user", :title, :description, :street, :zip, :city, :region, :lat, :lng, :websiteUrl, :phone, :placeKind, :openingHoursNote, :openingHoursJson, "manual", :placeStatus, NOW(), NOW())'
|
||||
);
|
||||
$placeStmt->execute([
|
||||
'uid' => $userId,
|
||||
@@ -547,12 +796,13 @@ final class ListingCatalog
|
||||
'placeKind' => $placeKind !== '' ? $placeKind : null,
|
||||
'openingHoursNote' => $openingHoursNote !== '' ? $openingHoursNote : null,
|
||||
'openingHoursJson' => $openingHoursRows !== [] ? json_encode($openingHoursRows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
|
||||
'placeStatus' => $placeStatus,
|
||||
]);
|
||||
$placeId = (int)$this->pdo->lastInsertId();
|
||||
|
||||
$listingStmt = $this->pdo->prepare(
|
||||
'INSERT INTO listings (created_by, owner_type, listing_type, primary_place_id, title, teaser_public, description, image_path, special_conditions_note, visibility, status, supports_registration, supports_capacity, supports_pricing, is_recurring, created_at, updated_at)
|
||||
VALUES (:uid, "user", :listingType, :placeId, :title, :teaser, :description, :imagePath, :specialConditionsNote, :visibility, "published", 0, :supportsCapacity, :supportsPricing, :isRecurring, NOW(), NOW())'
|
||||
VALUES (:uid, "user", :listingType, :placeId, :title, :teaser, :description, :imagePath, :specialConditionsNote, :visibility, :listingStatus, 0, :supportsCapacity, :supportsPricing, :isRecurring, NOW(), NOW())'
|
||||
);
|
||||
$listingStmt->execute([
|
||||
'uid' => $userId,
|
||||
@@ -564,6 +814,7 @@ final class ListingCatalog
|
||||
'imagePath' => $imagePath !== '' ? $imagePath : null,
|
||||
'specialConditionsNote' => $specialConditionsNote !== '' ? $specialConditionsNote : null,
|
||||
'visibility' => $visibility,
|
||||
'listingStatus' => $listingStatus,
|
||||
'supportsCapacity' => $capacityTotal !== null ? 1 : 0,
|
||||
'supportsPricing' => $this->hasPriceRows($data) || $specialConditionsNote !== '' ? 1 : 0,
|
||||
'isRecurring' => $entryType === 'editorial_event' && $recurrenceMode !== 'single' ? 1 : 0,
|
||||
@@ -582,10 +833,12 @@ final class ListingCatalog
|
||||
]);
|
||||
}
|
||||
|
||||
$this->pdo->commit();
|
||||
if ($managesTransaction) {
|
||||
$this->pdo->commit();
|
||||
}
|
||||
return (int)$listingId;
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
if ($managesTransaction && $this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
@@ -792,6 +1045,131 @@ final class ListingCatalog
|
||||
}
|
||||
}
|
||||
|
||||
private function createModerationRequest(int $listingId, string $requestType, int $requestedBy, ?string $reason, ?array $payload): int
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO listing_moderation_requests (listing_id, request_type, request_status, requested_by, request_reason, payload_json)
|
||||
VALUES (:listingId, :requestType, "open", :requestedBy, :requestReason, :payloadJson)'
|
||||
);
|
||||
$stmt->execute([
|
||||
'listingId' => $listingId,
|
||||
'requestType' => $requestType,
|
||||
'requestedBy' => $requestedBy,
|
||||
'requestReason' => trim((string)$reason) !== '' ? trim((string)$reason) : null,
|
||||
'payloadJson' => $payload !== null ? json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
|
||||
]);
|
||||
return (int)$this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
private function hasOpenModerationRequest(int $listingId, array $requestTypes): bool
|
||||
{
|
||||
$requestTypes = array_values(array_filter(array_map('strval', $requestTypes)));
|
||||
if ($requestTypes === []) {
|
||||
return false;
|
||||
}
|
||||
$placeholders = implode(',', array_fill(0, count($requestTypes), '?'));
|
||||
$params = array_merge([$listingId], $requestTypes);
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT id FROM listing_moderation_requests WHERE listing_id = ? AND request_status = 'open' AND request_type IN ($placeholders) LIMIT 1"
|
||||
);
|
||||
$stmt->execute($params);
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
private function buildModerationPayload(array $data, array $existing): array
|
||||
{
|
||||
$payload = $data;
|
||||
$fieldMap = [
|
||||
'title' => 'title',
|
||||
'description' => 'description',
|
||||
'teaser' => 'teaser_public',
|
||||
'street' => 'street',
|
||||
'zip' => 'zip',
|
||||
'city' => 'city',
|
||||
'region' => 'region',
|
||||
'lat' => 'lat',
|
||||
'lng' => 'lng',
|
||||
'visibility' => 'visibility',
|
||||
'category_slug' => 'category_slug',
|
||||
'image_path' => 'image_path',
|
||||
'special_conditions_note' => 'special_conditions_note',
|
||||
'phone' => 'phone',
|
||||
'website_url' => 'website_url',
|
||||
'opening_hours_note' => 'opening_hours_note',
|
||||
'starts_at' => 'starts_at',
|
||||
'recurrence_until' => 'recurrence_until',
|
||||
'recurrence_rule' => 'recurrence_rule',
|
||||
'entry_kind' => 'listing_type',
|
||||
];
|
||||
foreach ($fieldMap as $payloadKey => $existingKey) {
|
||||
if (!array_key_exists($payloadKey, $payload) || $payload[$payloadKey] === '') {
|
||||
$payload[$payloadKey] = $existing[$existingKey] ?? '';
|
||||
}
|
||||
}
|
||||
if (!isset($payload['category_input']) || trim((string)$payload['category_input']) === '') {
|
||||
$payload['category_input'] = $existing['category_slug'] ?? '';
|
||||
}
|
||||
if (!isset($payload['recurrence_mode']) || trim((string)$payload['recurrence_mode']) === '') {
|
||||
$payload['recurrence_mode'] = $this->recurrenceModeFromRule((string)($existing['recurrence_rule'] ?? ''));
|
||||
}
|
||||
if (!isset($payload['entry_kind']) || trim((string)$payload['entry_kind']) === '') {
|
||||
$payload['entry_kind'] = $existing['listing_type'] ?? 'editorial_event';
|
||||
}
|
||||
if (!isset($payload['capacity_total']) && isset($existing['capacity_total'])) {
|
||||
$payload['capacity_total'] = $existing['capacity_total'];
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function recurrenceModeFromRule(string $rule): string
|
||||
{
|
||||
if (str_contains($rule, 'FREQ=DAILY')) {
|
||||
return 'daily';
|
||||
}
|
||||
if (str_contains($rule, 'FREQ=WEEKLY')) {
|
||||
return 'weekly';
|
||||
}
|
||||
return 'single';
|
||||
}
|
||||
|
||||
private function assertNoDuplicateEntry(array $data, ?int $excludeListingId = null): void
|
||||
{
|
||||
$title = mb_strtolower(trim((string)($data['title'] ?? '')));
|
||||
$street = mb_strtolower(trim((string)($data['street'] ?? '')));
|
||||
$zip = trim((string)($data['zip'] ?? ''));
|
||||
$city = mb_strtolower(trim((string)($data['city'] ?? '')));
|
||||
if ($title === '' || ($street === '' && $zip === '' && $city === '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sql = 'SELECT l.id, l.title, lp.street, lp.zip, lp.city
|
||||
FROM listings l
|
||||
INNER JOIN listing_places lp ON lp.id = l.primary_place_id
|
||||
WHERE l.listing_type IN ("place","editorial_event")
|
||||
AND l.status IN ("draft","published")
|
||||
AND LOWER(TRIM(l.title)) = :title
|
||||
AND LOWER(TRIM(COALESCE(lp.street, ""))) = :street
|
||||
AND TRIM(COALESCE(lp.zip, "")) = :zip
|
||||
AND LOWER(TRIM(COALESCE(lp.city, ""))) = :city';
|
||||
$params = [
|
||||
'title' => $title,
|
||||
'street' => $street,
|
||||
'zip' => $zip,
|
||||
'city' => $city,
|
||||
];
|
||||
if ($excludeListingId !== null && $excludeListingId > 0) {
|
||||
$sql .= ' AND l.id <> :excludeId';
|
||||
$params['excludeId'] = $excludeListingId;
|
||||
}
|
||||
$sql .= ' LIMIT 1';
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$duplicate = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if ($duplicate) {
|
||||
throw new \RuntimeException('Ein ähnlicher Ort oder eine ähnliche Veranstaltung mit gleichem Namen und gleicher Adresse ist bereits vorhanden.');
|
||||
}
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
if (array_key_exists($table, $this->tableCache)) {
|
||||
|
||||
Reference in New Issue
Block a user