This commit is contained in:
@@ -12,6 +12,8 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
||||
- Die Community ist hierarchisch aufgebaut: Kategorien -> Boards -> Threads -> Posts.
|
||||
- Moderation ist vom normalen Community-Frontend getrennt.
|
||||
- Standortfunktionen sind relevant für lokale Events, Termine und Treffen.
|
||||
- Die bestehende `events`-Logik bleibt zunächst aktiv, wird aber künftig von einer allgemeineren Listing-/Ort-/Termin-Struktur abgelöst.
|
||||
- Seiten-Admins erhalten eine eigene System-Sektion für globale Betriebs- und Diensteschalter.
|
||||
|
||||
## Konto / Mitgliederbereich
|
||||
- Profil-Menü aktuell: `Profil`, `Kinder`, `Events`, `Community`, `Einstellungen`, `Abmelden`
|
||||
@@ -26,6 +28,11 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
||||
- Geschäftslogik unter `src/App/`
|
||||
- Globales Frontend vor allem über `public/assets/js/app.js` und `public/assets/css/app.css`
|
||||
|
||||
## Neue Ausbaustufe: Termin-/Ort-System
|
||||
- Neue Tabellenbasis: `listing_places`, `listings`, `listing_occurrences`, `listing_prices`, `listing_benefits`, `listing_categories`
|
||||
- Ziel: spätere Unterscheidung zwischen eigenem Event, Partner-Angebot, allgemeinem Ort und redaktionellem Termin
|
||||
- Google ist noch nicht aktiv integriert; das System wird nur vorbereitend über Settings und Datenfelder darauf ausgelegt
|
||||
|
||||
## Pflegehinweis
|
||||
Bei Änderungen immer auch die parallelen Dateien in `Internal/en/` prüfen und inhaltlich gleichziehen.
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ Stand: 2026-08-03
|
||||
- aktive Styles: `src/App/Avatar/Lorelei.php`, `src/App/Avatar/Croodles.php`
|
||||
- Profil-/Kinder-/Event-Logik im Mitgliederbereich: `partials/landing/account/dashboard.php`
|
||||
- Consent- und rechtliche Texte: `public/page/datenschutz.php`
|
||||
- System-Einstellungen: `src/App/SystemSettings.php`
|
||||
- neue Listing-/Ort-Basis: `src/App/ListingCatalog.php`
|
||||
|
||||
## Dokumentationsregel
|
||||
Kanonische interne Dokumentation:
|
||||
|
||||
@@ -20,6 +20,8 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
|
||||
- Konto-E-Mails sowie sensible Profilfelder werden app-seitig verschlüsselt gespeichert
|
||||
- Kinder können angelegt, bearbeitet und gelöscht werden
|
||||
- Profiladresse mit verschlüsselter Straße/Hausnummer, Adresssuche, Browser-Übernahme und Validierung
|
||||
- System-Sektion für Seiten-Admins mit Wartungs- und Diensteschaltern
|
||||
- neue Datenbasis für ein späteres Termin-, Ort- und Veranstaltungssystem angelegt
|
||||
|
||||
## Wording-Regel
|
||||
- Primärbegriff im Produkt: `Events`
|
||||
@@ -32,6 +34,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
|
||||
- App-Logik: `src/App/`
|
||||
- Assets: `public/assets/`
|
||||
- Datenbankschema: `schema.sql`
|
||||
- neue Basisklassen: `src/App/SystemSettings.php`, `src/App/ListingCatalog.php`
|
||||
|
||||
## Datenschutz und Sicherheit
|
||||
- Passwort-Hashes über Argon2id
|
||||
|
||||
@@ -12,6 +12,8 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
|
||||
- The community is hierarchical: categories -> boards -> threads -> posts.
|
||||
- Moderation is separated from the normal community frontend.
|
||||
- Location features are important for local discovery.
|
||||
- The existing `events` logic remains active for now, but future growth should move toward a broader listing, place, and occurrence model.
|
||||
- Site admins get a dedicated system section for global operating and service flags.
|
||||
|
||||
## Account / Member Area
|
||||
- Profile menu currently contains `Profile`, `Children`, `Events`, `Community`, `Settings`, `Logout`.
|
||||
@@ -26,6 +28,11 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
|
||||
- business logic under `src/App/`
|
||||
- global frontend mainly via `public/assets/js/app.js` and `public/assets/css/app.css`
|
||||
|
||||
## New Expansion Stage: Listings and Places
|
||||
- New table base: `listing_places`, `listings`, `listing_occurrences`, `listing_prices`, `listing_benefits`, `listing_categories`
|
||||
- Goal: later distinguish between own event, partner offer, general place, and editorial event entry
|
||||
- Google is not integrated productively yet; the system is only being prepared for it via settings and data fields
|
||||
|
||||
## Maintenance Note
|
||||
- Whenever one of these files changes, the matching files in `Internal/de/` and `Internal/en/` must be kept aligned.
|
||||
- The root `README.md` acts as the startup document for new chats and should be reviewed alongside major context changes.
|
||||
|
||||
@@ -16,6 +16,8 @@ Updated: 2026-08-03
|
||||
- active avatar styles: `src/App/Avatar/Lorelei.php`, `src/App/Avatar/Croodles.php`
|
||||
- profile/children/event logic inside member area: `partials/landing/account/dashboard.php`
|
||||
- consent and legal texts: `public/page/datenschutz.php`
|
||||
- system settings: `src/App/SystemSettings.php`
|
||||
- new listing/place base: `src/App/ListingCatalog.php`
|
||||
|
||||
## Documentation Rule
|
||||
- canonical internal documentation lives in `Internal/de/` and `Internal/en/`
|
||||
|
||||
@@ -20,6 +20,8 @@ Papa-Kind-Treff is a PHP-based platform for fathers focused on local events, app
|
||||
- account emails and sensitive profile fields are stored encrypted application-side
|
||||
- children can be created, edited, and deleted
|
||||
- profile address supports encrypted street/house number, address search, browser import, and validation
|
||||
- system section for site admins with maintenance and service toggles
|
||||
- new data foundation prepared for a broader event, place, and listing system
|
||||
|
||||
## Core Wording Rule
|
||||
- Primary product term: `Events`
|
||||
@@ -31,6 +33,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers focused on local events, app
|
||||
- application logic: `src/App/`
|
||||
- assets: `public/assets/`
|
||||
- database schema: `schema.sql`
|
||||
- new base classes: `src/App/SystemSettings.php`, `src/App/ListingCatalog.php`
|
||||
|
||||
## Privacy and Security
|
||||
- password hashes via Argon2id
|
||||
|
||||
@@ -27,12 +27,15 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf:
|
||||
- lokale Events, Termine und Treffen
|
||||
- Community-Austausch
|
||||
- geschützten Mitgliederbereich mit Profil, Kinderinfos und eigenen Events
|
||||
- neue Basis für ein übergreifendes Termin-, Ort- und Veranstaltungssystem
|
||||
|
||||
## Wichtige aktuelle Produktregeln
|
||||
- Primärbegriff im Produkt: `Events`
|
||||
- `Termine` und `Treffen` ergänzend in SEO- und Erklärungstexten
|
||||
- Hauptnavigation aktuell: `Home`, `Event Suche`, `Community`
|
||||
- Profil-Menü aktuell: `Profil`, `Kinder`, `Events`, `Community`, `Einstellungen`, `Abmelden`
|
||||
- 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
|
||||
|
||||
## Datenschutz und Sicherheit
|
||||
- sensible Profilfelder werden app-seitig verschlüsselt gespeichert
|
||||
|
||||
@@ -37,5 +37,47 @@ $app = \App\App::get();
|
||||
$app->session()->start();
|
||||
$clientId = $app->session()->ensureClientId();
|
||||
|
||||
try {
|
||||
$pdo = $app->pdo();
|
||||
if ($pdo) {
|
||||
$settings = new \App\SystemSettings($pdo);
|
||||
$settings->ensureSchema();
|
||||
if ($settings->getBool('site_maintenance_mode')) {
|
||||
$isAdminUser = false;
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$communityCfg = __DIR__ . '/community.php';
|
||||
$communityConfig = file_exists($communityCfg) ? require $communityCfg : [];
|
||||
$communityAccess = new \App\CommunityAccess($pdo, $communityConfig);
|
||||
$isAdminUser = $communityAccess->canManageApplications((int)$_SESSION['user_id']);
|
||||
}
|
||||
|
||||
$uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$uriPath = preg_replace('~/{2,}~', '/', $uriPath);
|
||||
$uriPath = trim($uriPath, '/');
|
||||
$maintenanceWhitelist = [
|
||||
'login',
|
||||
'logout',
|
||||
'verify',
|
||||
'reset',
|
||||
'impressum',
|
||||
'datenschutz',
|
||||
];
|
||||
|
||||
if (!$isAdminUser && !in_array($uriPath, $maintenanceWhitelist, true)) {
|
||||
http_response_code(503);
|
||||
header('Retry-After: 600');
|
||||
$message = htmlspecialchars(
|
||||
$settings->get('site_maintenance_message', 'Papa-Kind-Treff ist gerade kurz in Wartung. Bitte versuche es in Kürze erneut.') ?? '',
|
||||
ENT_QUOTES
|
||||
);
|
||||
echo '<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Wartung</title><style>body{margin:0;font-family:sans-serif;background:#f6efe5;color:#243b53;display:grid;place-items:center;min-height:100vh;padding:24px}.card{max-width:680px;background:#fff;border:1px solid #e5d7c3;border-radius:20px;padding:28px;box-shadow:0 20px 60px rgba(36,59,83,.08)}h1{margin:0 0 12px;font-size:2rem}p{margin:0;line-height:1.6;color:#486581}</style></head><body><main class="card"><h1>Papa-Kind-Treff ist kurz in Wartung</h1><p>' . $message . '</p></main></body></html>';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Fail open to avoid locking the whole site on transient settings/schema issues.
|
||||
}
|
||||
|
||||
// Optionally expose a single global for templates if desired
|
||||
$GLOBALS['client_id'] = $clientId;
|
||||
|
||||
@@ -20,6 +20,9 @@ $sectionLinks = [
|
||||
'community' => 'Community',
|
||||
'settings' => 'Einstellungen',
|
||||
];
|
||||
if (!empty($canManageSystemSettings)) {
|
||||
$sectionLinks['system'] = 'System';
|
||||
}
|
||||
?>
|
||||
<main class="section">
|
||||
<div class="container" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;">
|
||||
@@ -336,6 +339,76 @@ $sectionLinks = [
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($section === 'system' && !empty($canManageSystemSettings)): ?>
|
||||
<section class="account-panel">
|
||||
<div class="account-panel__head">
|
||||
<h2>System-Einstellungen</h2>
|
||||
<p class="muted">Globale Schalter für Wartung, externe Ortsdaten und die neue Termin-/Ort-Basis.</p>
|
||||
</div>
|
||||
<div class="account-panel__body">
|
||||
<form method="post" class="stack gap-12">
|
||||
<input type="hidden" name="action" value="system_settings_update">
|
||||
|
||||
<div class="card">
|
||||
<strong>Wartung und Verfügbarkeit</strong>
|
||||
<div class="stack gap-10" style="margin-top:12px;">
|
||||
<label class="chip" style="cursor:pointer; align-items:flex-start;">
|
||||
<input type="checkbox" name="forum_maintenance_mode" value="1" <?= !empty($systemSettingsValues['forum_maintenance_mode']) && $systemSettingsValues['forum_maintenance_mode'] === '1' ? 'checked' : '' ?> style="margin-right:8px; margin-top:4px;">
|
||||
<span>Forum-Wartungsmodus aktivieren
|
||||
<span class="muted small" style="display:block;">Die Community bleibt sichtbar, aber neue Themen und Antworten werden für normale Nutzer gesperrt.</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="chip" style="cursor:pointer; align-items:flex-start;">
|
||||
<input type="checkbox" name="site_maintenance_mode" value="1" <?= !empty($systemSettingsValues['site_maintenance_mode']) && $systemSettingsValues['site_maintenance_mode'] === '1' ? 'checked' : '' ?> style="margin-right:8px; margin-top:4px;">
|
||||
<span>Seiten-Wartungsmodus aktivieren
|
||||
<span class="muted small" style="display:block;">Normale Nutzer sehen dann statt der Website eine Wartungsseite. Seiten-Admins bleiben weiterhin im System.</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="siteMaintenanceMessage">Wartungshinweis</label>
|
||||
<textarea id="siteMaintenanceMessage" name="site_maintenance_message" class="textarea" rows="3"><?= htmlspecialchars((string)($systemSettingsValues['site_maintenance_message'] ?? ''), ENT_QUOTES) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<strong>Orts- und Bewertungsdienste</strong>
|
||||
<div class="stack gap-10" style="margin-top:12px;">
|
||||
<label class="chip" style="cursor:pointer; align-items:flex-start;">
|
||||
<input type="checkbox" name="google_places_enabled" value="1" <?= !empty($systemSettingsValues['google_places_enabled']) && $systemSettingsValues['google_places_enabled'] === '1' ? 'checked' : '' ?> style="margin-right:8px; margin-top:4px;">
|
||||
<span>Google Places vorbereitend aktivieren
|
||||
<span class="muted small" style="display:block;">Aktuell noch ohne produktive Anbindung. Der Schalter ist als technische Vorbereitung für spätere Place-ID-, Bewertungs- und Review-Anreicherung gedacht.</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="stack gap-6">
|
||||
<label class="label" for="placeDataProvider">Standardanbieter für Ortsdaten</label>
|
||||
<select id="placeDataProvider" name="place_data_provider" class="select">
|
||||
<option value="osm" <?= (($systemSettingsValues['place_data_provider'] ?? 'osm') === 'osm') ? 'selected' : '' ?>>OpenStreetMap / Nominatim</option>
|
||||
<option value="osm_google_optional" <?= (($systemSettingsValues['place_data_provider'] ?? 'osm') === 'osm_google_optional') ? 'selected' : '' ?>>OpenStreetMap primär, Google optional</option>
|
||||
</select>
|
||||
<p class="muted small">So bleibt eine kostenfreie Basis aktiv, während Google später zusätzlich vorbereitet werden kann.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-12">
|
||||
<button class="btn" type="submit">System-Einstellungen speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card" style="margin-top:18px;">
|
||||
<strong>Status der neuen Termin-/Ort-Basis</strong>
|
||||
<ul class="dash-list" style="margin-top:12px;">
|
||||
<li>Neue Tabellenbasis: <?= !empty($listingCatalogStatus['complete']) ? 'Vollständig initialisiert' : 'Noch unvollständig' ?></li>
|
||||
<li>Geplante Grundtypen: Event, Partner-Angebot, Ort, redaktioneller Termin</li>
|
||||
<li>Geplante Zusatzlogik: Kategorien, Serientermine, Preise, Gutscheine, Kapazitäten</li>
|
||||
<li>Fehlende Tabellen: <?= !empty($listingCatalogStatus['missing']) ? htmlspecialchars(implode(', ', $listingCatalogStatus['missing']), ENT_QUOTES) : 'Keine' ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,9 @@ $boardSlug = trim((string)($_GET['board'] ?? ''));
|
||||
$communityCfg = require __DIR__ . '/../../../config/community.php';
|
||||
$community = $pdo ? new \App\Community($pdo, $communityCfg) : null;
|
||||
$access = $pdo ? new \App\CommunityAccess($pdo, $communityCfg) : null;
|
||||
$systemSettings = $pdo ? new \App\SystemSettings($pdo) : null;
|
||||
$forumMaintenanceMode = $systemSettings ? $systemSettings->getBool('forum_maintenance_mode') : false;
|
||||
$isForumAdmin = ($access && $userId) ? $access->canModerateForum((int)$userId) : false;
|
||||
|
||||
$forumCategories = $community ? $community->listForumCategories() : [];
|
||||
$selectedBoard = ($community && $boardSlug !== '') ? $community->getBoardBySlug($boardSlug) : null;
|
||||
@@ -23,6 +26,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $community && $access) {
|
||||
if (!$userId) {
|
||||
throw new \RuntimeException('Bitte einloggen, um Themen zu erstellen.');
|
||||
}
|
||||
if ($forumMaintenanceMode && !$isForumAdmin) {
|
||||
throw new \RuntimeException('Die Community ist aktuell im Wartungsmodus. Neue Themen sind vorübergehend deaktiviert.');
|
||||
}
|
||||
if (!$access->canCreateThread((int)$userId)) {
|
||||
$state = $access->getRestrictionState((int)$userId);
|
||||
throw new \RuntimeException($state['reason'] ?: 'Du darfst aktuell keine Themen erstellen.');
|
||||
@@ -67,8 +73,10 @@ $threads = $community
|
||||
</div>
|
||||
</form>
|
||||
<div class="forum-hero__cta">
|
||||
<?php if ($userId): ?>
|
||||
<?php if ($userId && !$forumMaintenanceMode): ?>
|
||||
<button class="btn" type="button" data-modal-open="modalThread">Neues Thema</button>
|
||||
<?php elseif ($userId): ?>
|
||||
<button class="btn ghost" type="button" disabled>Wartungsmodus aktiv</button>
|
||||
<?php else: ?>
|
||||
<a class="btn" href="/login">Einloggen und schreiben</a>
|
||||
<?php endif; ?>
|
||||
@@ -82,6 +90,11 @@ $threads = $community
|
||||
<?php if ($info): ?>
|
||||
<div class="toast-bar" style="margin-top:14px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($forumMaintenanceMode): ?>
|
||||
<div class="toast-bar" style="margin-top:14px; border-color:#d4a857; color:#7c5a17;">
|
||||
Die Community ist aktuell im Wartungsmodus. Lesen bleibt möglich, neue Themen und Antworten sind für normale Nutzer vorübergehend deaktiviert.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($selectedBoard): ?>
|
||||
<div class="forum-shell">
|
||||
@@ -239,7 +252,7 @@ $threads = $community
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php if ($userId): ?>
|
||||
<?php if ($userId && !$forumMaintenanceMode): ?>
|
||||
<div class="modal" id="modalThread">
|
||||
<div class="panel">
|
||||
<div class="head flex between center-y">
|
||||
|
||||
@@ -12,6 +12,8 @@ $editPostId = (int)($_GET['edit_post'] ?? 0);
|
||||
$communityCfg = require __DIR__ . '/../../../config/community.php';
|
||||
$community = $pdo ? new \App\Community($pdo, $communityCfg) : null;
|
||||
$access = $pdo ? new \App\CommunityAccess($pdo, $communityCfg) : null;
|
||||
$systemSettings = $pdo ? new \App\SystemSettings($pdo) : null;
|
||||
$forumMaintenanceMode = $systemSettings ? $systemSettings->getBool('forum_maintenance_mode') : false;
|
||||
$forumCategories = $community ? $community->listForumCategories() : [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $community && $access) {
|
||||
@@ -21,6 +23,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $community && $access) {
|
||||
if (!$userId) {
|
||||
throw new \RuntimeException('Bitte einloggen, um zu antworten.');
|
||||
}
|
||||
if ($forumMaintenanceMode && !($access && $access->canModerateForum((int)$userId))) {
|
||||
throw new \RuntimeException('Die Community ist aktuell im Wartungsmodus. Antworten sind vorübergehend deaktiviert.');
|
||||
}
|
||||
if (!$access->canReply((int)$userId)) {
|
||||
$state = $access->getRestrictionState((int)$userId);
|
||||
throw new \RuntimeException($state['reason'] ?: 'Du darfst aktuell nicht antworten.');
|
||||
@@ -151,6 +156,11 @@ $userVotes = ($access && $userId) ? $access->getUserPostVotes((int)$userId, $pos
|
||||
<?php if ($info): ?>
|
||||
<div class="toast-bar" style="margin-bottom:14px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($forumMaintenanceMode): ?>
|
||||
<div class="toast-bar" style="margin-bottom:14px; border-color:#d4a857; color:#7c5a17;">
|
||||
Die Community ist aktuell im Wartungsmodus. Lesen bleibt möglich, neue Antworten nur für Admins.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<article class="forum-post forum-post--lead">
|
||||
<aside class="forum-post__author">
|
||||
@@ -290,7 +300,7 @@ $userVotes = ($access && $userId) ? $access->getUserPostVotes((int)$userId, $pos
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($userId): ?>
|
||||
<?php if ($userId && !$forumMaintenanceMode): ?>
|
||||
<?php if ($userRestrictions['reply_blocked']): ?>
|
||||
<div class="forum-login-note">Du darfst aktuell keine Antworten schreiben. <?= !empty($userRestrictions['reason']) ? htmlspecialchars((string)$userRestrictions['reason'], ENT_QUOTES) : '' ?></div>
|
||||
<?php else: ?>
|
||||
@@ -303,6 +313,8 @@ $userVotes = ($access && $userId) ? $access->getUserPostVotes((int)$userId, $pos
|
||||
<button class="btn" type="submit">Antwort senden</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($userId): ?>
|
||||
<div class="forum-login-note">Die Community ist aktuell im Wartungsmodus. Antworten sind vorübergehend deaktiviert.</div>
|
||||
<?php else: ?>
|
||||
<div class="forum-login-note">Bitte <a href="/login">einloggen</a>, um zu antworten.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -15,6 +15,7 @@ if (!in_array($childGender, ['male', 'female', 'mixed'], true)) {
|
||||
$debugEnabled = defined('APP_DEBUG') && APP_DEBUG === true;
|
||||
$locationTrackingPreference = 'prompt';
|
||||
$showDebugTools = false;
|
||||
$adminSystemNotice = null;
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
try {
|
||||
$pdo = $app->pdo();
|
||||
@@ -25,10 +26,30 @@ if (isset($_SESSION['user_id'])) {
|
||||
$communityConfig = file_exists($communityCfg) ? require $communityCfg : [];
|
||||
$communityAccess = new \App\CommunityAccess($pdo, $communityConfig);
|
||||
$showDebugTools = $debugEnabled && $communityAccess->hasRole((int)$_SESSION['user_id'], 'site_admin');
|
||||
if ($communityAccess->canManageApplications((int)$_SESSION['user_id'])) {
|
||||
$systemSettings = new \App\SystemSettings($pdo);
|
||||
$siteMaintenanceMode = $systemSettings->getBool('site_maintenance_mode');
|
||||
$forumMaintenanceMode = $systemSettings->getBool('forum_maintenance_mode');
|
||||
$googlePlacesEnabled = $systemSettings->getBool('google_places_enabled');
|
||||
$parts = [];
|
||||
if ($siteMaintenanceMode) {
|
||||
$parts[] = 'Seiten-Wartungsmodus aktiv';
|
||||
}
|
||||
if ($forumMaintenanceMode) {
|
||||
$parts[] = 'Forum-Wartungsmodus aktiv';
|
||||
}
|
||||
if ($googlePlacesEnabled) {
|
||||
$parts[] = 'Google-Places-Vorbereitung aktiv';
|
||||
}
|
||||
if ($parts !== []) {
|
||||
$adminSystemNotice = implode(' · ', $parts);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$locationTrackingPreference = 'prompt';
|
||||
$showDebugTools = false;
|
||||
$adminSystemNotice = null;
|
||||
}
|
||||
}
|
||||
$debugFiles = [];
|
||||
@@ -105,6 +126,11 @@ if ($debugEnabled) {
|
||||
</script>
|
||||
<?php tpl('matomo', 'structure'); ?>
|
||||
<?php tpl('nav', 'structure'); ?>
|
||||
<?php if ($adminSystemNotice !== null): ?>
|
||||
<div class="container" style="margin-top:14px;">
|
||||
<div class="toast-bar" style="border-color:#d4a857; color:#7c5a17;"><?= htmlspecialchars($adminSystemNotice, ENT_QUOTES) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="cookie-consent" id="cookieConsentBanner" hidden>
|
||||
<div class="cookie-consent__inner">
|
||||
<div class="cookie-consent__copy">
|
||||
|
||||
@@ -4,6 +4,7 @@ $isLoggedIn = isset($_SESSION['user_id']);
|
||||
$displayName = 'Profil';
|
||||
$profileInitial = 'P';
|
||||
$showAdminLink = false;
|
||||
$showSystemLink = false;
|
||||
|
||||
if ($isLoggedIn) {
|
||||
try {
|
||||
@@ -25,12 +26,14 @@ if ($isLoggedIn) {
|
||||
$communityConfig = require $communityCfg;
|
||||
$communityAccess = new \App\CommunityAccess($pdo, $communityConfig);
|
||||
$showAdminLink = $communityAccess->canModerateForum((int)$_SESSION['user_id']);
|
||||
$showSystemLink = $communityAccess->canManageApplications((int)$_SESSION['user_id']);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$displayName = 'Profil';
|
||||
$profileInitial = 'P';
|
||||
$showAdminLink = false;
|
||||
$showSystemLink = false;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -61,6 +64,9 @@ if ($isLoggedIn) {
|
||||
<a href="/dashboard?section=events" role="menuitem">Events</a>
|
||||
<a href="/dashboard?section=community" role="menuitem">Community</a>
|
||||
<a href="/dashboard?section=settings" role="menuitem">Einstellungen</a>
|
||||
<?php if ($showSystemLink): ?>
|
||||
<a href="/dashboard?section=system" role="menuitem">System</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($showAdminLink): ?>
|
||||
<a href="/community-admin" role="menuitem">Admin</a>
|
||||
<?php endif; ?>
|
||||
@@ -84,6 +90,9 @@ if ($isLoggedIn) {
|
||||
<a class="btn ghost" href="/dashboard?section=events">Events</a>
|
||||
<a class="btn ghost" href="/dashboard?section=community">Community</a>
|
||||
<a class="btn ghost" href="/dashboard?section=settings">Einstellungen</a>
|
||||
<?php if ($showSystemLink): ?>
|
||||
<a class="btn ghost" href="/dashboard?section=system">System</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($showAdminLink): ?>
|
||||
<a class="btn ghost" href="/community-admin">Admin</a>
|
||||
<?php endif; ?>
|
||||
|
||||
137
schema.sql
137
schema.sql
@@ -115,6 +115,143 @@ CREATE TABLE event_participants (
|
||||
CONSTRAINT fk_ep_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE 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;
|
||||
|
||||
CREATE TABLE 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 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 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 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 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 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 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;
|
||||
|
||||
-- Community / Forum
|
||||
CREATE TABLE forum_categories (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
|
||||
@@ -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