This commit is contained in:
@@ -174,8 +174,21 @@ final class AccountPages
|
||||
$communityAccess = $pdo ? new CommunityAccess($pdo, $communityConfig) : null;
|
||||
$communityMigration = $pdo ? new CommunityMigration($pdo) : null;
|
||||
$profileSettings = $pdo ? new ProfileSettings($pdo) : null;
|
||||
$systemSettings = $pdo ? new SystemSettings($pdo) : null;
|
||||
$listingCatalog = $pdo ? new ListingCatalog($pdo) : null;
|
||||
$section = (string)($_GET['section'] ?? 'profile');
|
||||
$canManageSystemSettings = $communityAccess ? $communityAccess->canManageApplications($userId) : false;
|
||||
$allowedSections = ['profile', 'children', 'events', 'community', 'settings'];
|
||||
if ($canManageSystemSettings) {
|
||||
$allowedSections[] = 'system';
|
||||
}
|
||||
|
||||
if ($systemSettings) {
|
||||
$systemSettings->ensureSchema();
|
||||
}
|
||||
if ($listingCatalog) {
|
||||
$listingCatalog->ensureSchema();
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
@@ -273,6 +286,26 @@ final class AccountPages
|
||||
);
|
||||
}
|
||||
$info = 'Einstellungen gespeichert.';
|
||||
} elseif ($action === 'system_settings_update') {
|
||||
if (!$canManageSystemSettings || !$systemSettings) {
|
||||
throw new \RuntimeException('Keine Berechtigung für die System-Einstellungen.');
|
||||
}
|
||||
$siteMaintenanceMessage = trim((string)($_POST['site_maintenance_message'] ?? ''));
|
||||
if ($siteMaintenanceMessage === '') {
|
||||
$siteMaintenanceMessage = 'Papa-Kind-Treff ist gerade kurz in Wartung. Bitte versuche es in Kürze erneut.';
|
||||
}
|
||||
$placeDataProvider = (string)($_POST['place_data_provider'] ?? 'osm');
|
||||
if (!in_array($placeDataProvider, ['osm', 'osm_google_optional'], true)) {
|
||||
$placeDataProvider = 'osm';
|
||||
}
|
||||
$systemSettings->updateMany([
|
||||
'google_places_enabled' => isset($_POST['google_places_enabled']) ? '1' : '0',
|
||||
'forum_maintenance_mode' => isset($_POST['forum_maintenance_mode']) ? '1' : '0',
|
||||
'site_maintenance_mode' => isset($_POST['site_maintenance_mode']) ? '1' : '0',
|
||||
'site_maintenance_message' => $siteMaintenanceMessage,
|
||||
'place_data_provider' => $placeDataProvider,
|
||||
], $userId);
|
||||
$info = 'System-Einstellungen gespeichert.';
|
||||
} elseif ($action === 'child_add' || $action === 'child_update') {
|
||||
$crypto = self::requireCrypto($crypto, 'Kinder');
|
||||
$childId = (int)($_POST['child_id'] ?? 0);
|
||||
@@ -588,6 +621,8 @@ final class AccountPages
|
||||
'reply_blocked' => false,
|
||||
'reason' => null,
|
||||
];
|
||||
$systemSettingsValues = $systemSettings ? $systemSettings->getAll() : [];
|
||||
$listingCatalogStatus = $listingCatalog ? $listingCatalog->status() : ['complete' => false, 'missing' => [], 'tables' => []];
|
||||
if (!in_array($section, $allowedSections, true)) {
|
||||
$section = 'profile';
|
||||
}
|
||||
@@ -609,6 +644,9 @@ final class AccountPages
|
||||
'communityApplication',
|
||||
'communityCanApply',
|
||||
'communityRestrictions',
|
||||
'canManageSystemSettings',
|
||||
'systemSettingsValues',
|
||||
'listingCatalogStatus',
|
||||
'avatarBuilder',
|
||||
'section',
|
||||
'allowedSections'
|
||||
|
||||
214
src/App/ListingCatalog.php
Normal file
214
src/App/ListingCatalog.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class ListingCatalog
|
||||
{
|
||||
private array $tableCache = [];
|
||||
|
||||
public function __construct(private \PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
public function ensureSchema(): void
|
||||
{
|
||||
$statements = [
|
||||
'CREATE TABLE IF NOT EXISTS listing_categories (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(120) NOT NULL UNIQUE,
|
||||
title VARCHAR(160) NOT NULL,
|
||||
category_group ENUM("general","event","place","food","family","partner") NOT NULL DEFAULT "general",
|
||||
sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
'CREATE TABLE IF NOT EXISTS listing_places (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
created_by BIGINT UNSIGNED NULL,
|
||||
source_type ENUM("user","partner","admin","system") NOT NULL DEFAULT "user",
|
||||
title VARCHAR(180) NOT NULL,
|
||||
description TEXT NULL,
|
||||
street VARCHAR(180) NULL,
|
||||
zip CHAR(5) NULL,
|
||||
city VARCHAR(120) NULL,
|
||||
region VARCHAR(120) NULL,
|
||||
lat DECIMAL(10,7) NULL,
|
||||
lng DECIMAL(10,7) NULL,
|
||||
website_url VARCHAR(255) NULL,
|
||||
phone VARCHAR(60) NULL,
|
||||
place_kind VARCHAR(80) NULL,
|
||||
provider_hint ENUM("manual","osm","google") NOT NULL DEFAULT "manual",
|
||||
external_place_id VARCHAR(190) NULL,
|
||||
google_place_id VARCHAR(190) NULL,
|
||||
rating_value DECIMAL(3,2) NULL,
|
||||
rating_count INT UNSIGNED NULL,
|
||||
status ENUM("draft","published","archived") NOT NULL DEFAULT "published",
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_listing_places_user FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
INDEX idx_listing_places_city (city),
|
||||
INDEX idx_listing_places_region (region),
|
||||
INDEX idx_listing_places_latlng (lat, lng),
|
||||
INDEX idx_listing_places_kind (place_kind)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
'CREATE TABLE IF NOT EXISTS listings (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
created_by BIGINT UNSIGNED NULL,
|
||||
owner_type ENUM("user","partner","admin","system") NOT NULL DEFAULT "user",
|
||||
listing_type ENUM("event","partner_offer","place","editorial_event") NOT NULL DEFAULT "event",
|
||||
primary_place_id BIGINT UNSIGNED NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
teaser_public VARCHAR(280) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
visibility ENUM("public","members") NOT NULL DEFAULT "public",
|
||||
status ENUM("draft","published","cancelled","archived") NOT NULL DEFAULT "draft",
|
||||
supports_registration TINYINT(1) NOT NULL DEFAULT 0,
|
||||
supports_capacity TINYINT(1) NOT NULL DEFAULT 0,
|
||||
supports_pricing TINYINT(1) NOT NULL DEFAULT 0,
|
||||
is_recurring TINYINT(1) NOT NULL DEFAULT 0,
|
||||
legacy_event_id BIGINT UNSIGNED NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_listings_user FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_listings_place FOREIGN KEY (primary_place_id) REFERENCES listing_places(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_listings_legacy_event FOREIGN KEY (legacy_event_id) REFERENCES events(id) ON DELETE SET NULL,
|
||||
INDEX idx_listings_type (listing_type),
|
||||
INDEX idx_listings_status (status),
|
||||
INDEX idx_listings_owner (owner_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
'CREATE TABLE IF NOT EXISTS listing_category_map (
|
||||
listing_id BIGINT UNSIGNED NOT NULL,
|
||||
category_id BIGINT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (listing_id, category_id),
|
||||
CONSTRAINT fk_lcm_listing FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_lcm_category FOREIGN KEY (category_id) REFERENCES listing_categories(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
'CREATE TABLE IF NOT EXISTS listing_occurrences (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
listing_id BIGINT UNSIGNED NOT NULL,
|
||||
occurrence_type ENUM("single","series","range","open_ended") NOT NULL DEFAULT "single",
|
||||
starts_at DATETIME NULL,
|
||||
ends_at DATETIME NULL,
|
||||
recurrence_rule VARCHAR(255) NULL,
|
||||
recurrence_until DATETIME NULL,
|
||||
capacity_total SMALLINT UNSIGNED NULL,
|
||||
booking_url VARCHAR(255) NULL,
|
||||
status ENUM("scheduled","cancelled","sold_out") NOT NULL DEFAULT "scheduled",
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_listing_occurrences_listing FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE,
|
||||
INDEX idx_listing_occurrences_start (starts_at),
|
||||
INDEX idx_listing_occurrences_type (occurrence_type),
|
||||
INDEX idx_listing_occurrences_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
'CREATE TABLE IF NOT EXISTS listing_prices (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
listing_id BIGINT UNSIGNED NOT NULL,
|
||||
occurrence_id BIGINT UNSIGNED NULL,
|
||||
label VARCHAR(120) NOT NULL,
|
||||
audience ENUM("general","adult","child","family","group") NOT NULL DEFAULT "general",
|
||||
price_type ENUM("free","fixed","from","up_to","range","request") NOT NULL DEFAULT "fixed",
|
||||
amount DECIMAL(10,2) NULL,
|
||||
amount_secondary DECIMAL(10,2) NULL,
|
||||
currency CHAR(3) NOT NULL DEFAULT "EUR",
|
||||
note VARCHAR(255) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_listing_prices_listing FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_listing_prices_occurrence FOREIGN KEY (occurrence_id) REFERENCES listing_occurrences(id) ON DELETE CASCADE,
|
||||
INDEX idx_listing_prices_listing (listing_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
'CREATE TABLE IF NOT EXISTS listing_benefits (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
listing_id BIGINT UNSIGNED NOT NULL,
|
||||
occurrence_id BIGINT UNSIGNED NULL,
|
||||
benefit_type ENUM("voucher_code","voucher_hint","discount_text") NOT NULL DEFAULT "voucher_hint",
|
||||
title VARCHAR(160) NOT NULL,
|
||||
code VARCHAR(120) NULL,
|
||||
description TEXT NULL,
|
||||
valid_from DATETIME NULL,
|
||||
valid_until DATETIME NULL,
|
||||
is_public TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_listing_benefits_listing FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE,
|
||||
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',
|
||||
];
|
||||
|
||||
foreach ($statements as $sql) {
|
||||
$this->pdo->exec($sql);
|
||||
}
|
||||
|
||||
$seed = $this->pdo->prepare(
|
||||
'INSERT INTO listing_categories (slug, title, category_group, sort_order)
|
||||
VALUES (:slug, :title, :groupName, :sortOrder)
|
||||
ON DUPLICATE KEY UPDATE title = VALUES(title), category_group = VALUES(category_group), sort_order = VALUES(sort_order), updated_at = CURRENT_TIMESTAMP'
|
||||
);
|
||||
foreach ($this->defaultCategories() as $index => $category) {
|
||||
$seed->execute([
|
||||
'slug' => $category['slug'],
|
||||
'title' => $category['title'],
|
||||
'groupName' => $category['group'],
|
||||
'sortOrder' => $index + 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function status(): array
|
||||
{
|
||||
$tables = [
|
||||
'listing_categories',
|
||||
'listing_places',
|
||||
'listings',
|
||||
'listing_category_map',
|
||||
'listing_occurrences',
|
||||
'listing_prices',
|
||||
'listing_benefits',
|
||||
];
|
||||
$missing = [];
|
||||
foreach ($tables as $table) {
|
||||
if (!$this->hasTable($table)) {
|
||||
$missing[] = $table;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'complete' => $missing === [],
|
||||
'missing' => $missing,
|
||||
'tables' => $tables,
|
||||
];
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
if (array_key_exists($table, $this->tableCache)) {
|
||||
return $this->tableCache[$table];
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->pdo->prepare('SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table LIMIT 1');
|
||||
$stmt->execute(['table' => $table]);
|
||||
return $this->tableCache[$table] = (bool)$stmt->fetchColumn();
|
||||
} catch (\Throwable) {
|
||||
return $this->tableCache[$table] = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function defaultCategories(): array
|
||||
{
|
||||
return [
|
||||
['slug' => 'spielplatz', 'title' => 'Spielplatz', 'group' => 'place'],
|
||||
['slug' => 'cafe', 'title' => 'Café', 'group' => 'food'],
|
||||
['slug' => 'restaurant', 'title' => 'Restaurant', 'group' => 'food'],
|
||||
['slug' => 'indoor-spielort', 'title' => 'Indoor-Spielort', 'group' => 'place'],
|
||||
['slug' => 'zirkus', 'title' => 'Zirkus', 'group' => 'event'],
|
||||
['slug' => 'huepfburgen', 'title' => 'Hüpfburgen', 'group' => 'event'],
|
||||
['slug' => 'workshop', 'title' => 'Workshop', 'group' => 'event'],
|
||||
['slug' => 'kurs', 'title' => 'Kurs', 'group' => 'event'],
|
||||
['slug' => 'familienfest', 'title' => 'Familienfest', 'group' => 'family'],
|
||||
];
|
||||
}
|
||||
}
|
||||
107
src/App/SystemSettings.php
Normal file
107
src/App/SystemSettings.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class SystemSettings
|
||||
{
|
||||
private array $tableCache = [];
|
||||
|
||||
private const DEFAULTS = [
|
||||
'google_places_enabled' => '0',
|
||||
'forum_maintenance_mode' => '0',
|
||||
'site_maintenance_mode' => '0',
|
||||
'site_maintenance_message' => 'Papa-Kind-Treff ist gerade kurz in Wartung. Bitte versuche es in Kürze erneut.',
|
||||
'place_data_provider' => 'osm',
|
||||
];
|
||||
|
||||
public function __construct(private \PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
public function ensureSchema(): void
|
||||
{
|
||||
$this->pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS system_settings (
|
||||
`key` VARCHAR(120) NOT NULL PRIMARY KEY,
|
||||
`value` TEXT NULL,
|
||||
updated_by BIGINT UNSIGNED NULL,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_system_settings_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
||||
);
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO system_settings (`key`, `value`, updated_by)
|
||||
VALUES (:key, :value, NULL)
|
||||
ON DUPLICATE KEY UPDATE `value` = `value`'
|
||||
);
|
||||
foreach (self::DEFAULTS as $key => $value) {
|
||||
$stmt->execute([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$settings = self::DEFAULTS;
|
||||
$stmt = $this->pdo->query('SELECT `key`, `value` FROM system_settings');
|
||||
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) {
|
||||
$settings[(string)$row['key']] = (string)($row['value'] ?? '');
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function get(string $key, ?string $default = null): ?string
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->prepare('SELECT `value` FROM system_settings WHERE `key` = :key LIMIT 1');
|
||||
$stmt->execute(['key' => $key]);
|
||||
$value = $stmt->fetchColumn();
|
||||
if ($value === false) {
|
||||
return $default ?? (self::DEFAULTS[$key] ?? null);
|
||||
}
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
public function getBool(string $key, bool $default = false): bool
|
||||
{
|
||||
$value = $this->get($key, $default ? '1' : '0');
|
||||
return in_array((string)$value, ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
public function set(string $key, string $value, ?int $updatedBy = null): void
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO system_settings (`key`, `value`, updated_by)
|
||||
VALUES (:key, :value, :updatedBy)
|
||||
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), updated_by = VALUES(updated_by), updated_at = CURRENT_TIMESTAMP'
|
||||
);
|
||||
$stmt->execute([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'updatedBy' => $updatedBy,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateMany(array $values, ?int $updatedBy = null): void
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO system_settings (`key`, `value`, updated_by)
|
||||
VALUES (:key, :value, :updatedBy)
|
||||
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), updated_by = VALUES(updated_by), updated_at = CURRENT_TIMESTAMP'
|
||||
);
|
||||
foreach ($values as $key => $value) {
|
||||
$stmt->execute([
|
||||
'key' => (string)$key,
|
||||
'value' => (string)$value,
|
||||
'updatedBy' => $updatedBy,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user