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.
|
- Die Community ist hierarchisch aufgebaut: Kategorien -> Boards -> Threads -> Posts.
|
||||||
- Moderation ist vom normalen Community-Frontend getrennt.
|
- Moderation ist vom normalen Community-Frontend getrennt.
|
||||||
- Standortfunktionen sind relevant für lokale Events, Termine und Treffen.
|
- 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
|
## Konto / Mitgliederbereich
|
||||||
- Profil-Menü aktuell: `Profil`, `Kinder`, `Events`, `Community`, `Einstellungen`, `Abmelden`
|
- 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/`
|
- Geschäftslogik unter `src/App/`
|
||||||
- Globales Frontend vor allem über `public/assets/js/app.js` und `public/assets/css/app.css`
|
- 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
|
## Pflegehinweis
|
||||||
Bei Änderungen immer auch die parallelen Dateien in `Internal/en/` prüfen und inhaltlich gleichziehen.
|
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`
|
- aktive Styles: `src/App/Avatar/Lorelei.php`, `src/App/Avatar/Croodles.php`
|
||||||
- Profil-/Kinder-/Event-Logik im Mitgliederbereich: `partials/landing/account/dashboard.php`
|
- Profil-/Kinder-/Event-Logik im Mitgliederbereich: `partials/landing/account/dashboard.php`
|
||||||
- Consent- und rechtliche Texte: `public/page/datenschutz.php`
|
- Consent- und rechtliche Texte: `public/page/datenschutz.php`
|
||||||
|
- System-Einstellungen: `src/App/SystemSettings.php`
|
||||||
|
- neue Listing-/Ort-Basis: `src/App/ListingCatalog.php`
|
||||||
|
|
||||||
## Dokumentationsregel
|
## Dokumentationsregel
|
||||||
Kanonische interne Dokumentation:
|
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
|
- Konto-E-Mails sowie sensible Profilfelder werden app-seitig verschlüsselt gespeichert
|
||||||
- Kinder können angelegt, bearbeitet und gelöscht werden
|
- Kinder können angelegt, bearbeitet und gelöscht werden
|
||||||
- Profiladresse mit verschlüsselter Straße/Hausnummer, Adresssuche, Browser-Übernahme und Validierung
|
- 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
|
## Wording-Regel
|
||||||
- Primärbegriff im Produkt: `Events`
|
- 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/`
|
- App-Logik: `src/App/`
|
||||||
- Assets: `public/assets/`
|
- Assets: `public/assets/`
|
||||||
- Datenbankschema: `schema.sql`
|
- Datenbankschema: `schema.sql`
|
||||||
|
- neue Basisklassen: `src/App/SystemSettings.php`, `src/App/ListingCatalog.php`
|
||||||
|
|
||||||
## Datenschutz und Sicherheit
|
## Datenschutz und Sicherheit
|
||||||
- Passwort-Hashes über Argon2id
|
- 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.
|
- The community is hierarchical: categories -> boards -> threads -> posts.
|
||||||
- Moderation is separated from the normal community frontend.
|
- Moderation is separated from the normal community frontend.
|
||||||
- Location features are important for local discovery.
|
- 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
|
## Account / Member Area
|
||||||
- Profile menu currently contains `Profile`, `Children`, `Events`, `Community`, `Settings`, `Logout`.
|
- 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/`
|
- business logic under `src/App/`
|
||||||
- global frontend mainly via `public/assets/js/app.js` and `public/assets/css/app.css`
|
- 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
|
## Maintenance Note
|
||||||
- Whenever one of these files changes, the matching files in `Internal/de/` and `Internal/en/` must be kept aligned.
|
- 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.
|
- 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`
|
- 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`
|
- profile/children/event logic inside member area: `partials/landing/account/dashboard.php`
|
||||||
- consent and legal texts: `public/page/datenschutz.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
|
## Documentation Rule
|
||||||
- canonical internal documentation lives in `Internal/de/` and `Internal/en/`
|
- 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
|
- account emails and sensitive profile fields are stored encrypted application-side
|
||||||
- children can be created, edited, and deleted
|
- children can be created, edited, and deleted
|
||||||
- profile address supports encrypted street/house number, address search, browser import, and validation
|
- 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
|
## Core Wording Rule
|
||||||
- Primary product term: `Events`
|
- 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/`
|
- application logic: `src/App/`
|
||||||
- assets: `public/assets/`
|
- assets: `public/assets/`
|
||||||
- database schema: `schema.sql`
|
- database schema: `schema.sql`
|
||||||
|
- new base classes: `src/App/SystemSettings.php`, `src/App/ListingCatalog.php`
|
||||||
|
|
||||||
## Privacy and Security
|
## Privacy and Security
|
||||||
- password hashes via Argon2id
|
- 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
|
- lokale Events, Termine und Treffen
|
||||||
- Community-Austausch
|
- Community-Austausch
|
||||||
- geschützten Mitgliederbereich mit Profil, Kinderinfos und eigenen Events
|
- geschützten Mitgliederbereich mit Profil, Kinderinfos und eigenen Events
|
||||||
|
- neue Basis für ein übergreifendes Termin-, Ort- und Veranstaltungssystem
|
||||||
|
|
||||||
## Wichtige aktuelle Produktregeln
|
## Wichtige aktuelle Produktregeln
|
||||||
- Primärbegriff im Produkt: `Events`
|
- Primärbegriff im Produkt: `Events`
|
||||||
- `Termine` und `Treffen` ergänzend in SEO- und Erklärungstexten
|
- `Termine` und `Treffen` ergänzend in SEO- und Erklärungstexten
|
||||||
- Hauptnavigation aktuell: `Home`, `Event Suche`, `Community`
|
- Hauptnavigation aktuell: `Home`, `Event Suche`, `Community`
|
||||||
- Profil-Menü aktuell: `Profil`, `Kinder`, `Events`, `Community`, `Einstellungen`, `Abmelden`
|
- 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
|
## Datenschutz und Sicherheit
|
||||||
- sensible Profilfelder werden app-seitig verschlüsselt gespeichert
|
- sensible Profilfelder werden app-seitig verschlüsselt gespeichert
|
||||||
|
|||||||
@@ -37,5 +37,47 @@ $app = \App\App::get();
|
|||||||
$app->session()->start();
|
$app->session()->start();
|
||||||
$clientId = $app->session()->ensureClientId();
|
$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
|
// Optionally expose a single global for templates if desired
|
||||||
$GLOBALS['client_id'] = $clientId;
|
$GLOBALS['client_id'] = $clientId;
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ $sectionLinks = [
|
|||||||
'community' => 'Community',
|
'community' => 'Community',
|
||||||
'settings' => 'Einstellungen',
|
'settings' => 'Einstellungen',
|
||||||
];
|
];
|
||||||
|
if (!empty($canManageSystemSettings)) {
|
||||||
|
$sectionLinks['system'] = 'System';
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<main class="section">
|
<main class="section">
|
||||||
<div class="container" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;">
|
<div class="container" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;">
|
||||||
@@ -336,6 +339,76 @@ $sectionLinks = [
|
|||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ $boardSlug = trim((string)($_GET['board'] ?? ''));
|
|||||||
$communityCfg = require __DIR__ . '/../../../config/community.php';
|
$communityCfg = require __DIR__ . '/../../../config/community.php';
|
||||||
$community = $pdo ? new \App\Community($pdo, $communityCfg) : null;
|
$community = $pdo ? new \App\Community($pdo, $communityCfg) : null;
|
||||||
$access = $pdo ? new \App\CommunityAccess($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() : [];
|
$forumCategories = $community ? $community->listForumCategories() : [];
|
||||||
$selectedBoard = ($community && $boardSlug !== '') ? $community->getBoardBySlug($boardSlug) : null;
|
$selectedBoard = ($community && $boardSlug !== '') ? $community->getBoardBySlug($boardSlug) : null;
|
||||||
@@ -23,6 +26,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $community && $access) {
|
|||||||
if (!$userId) {
|
if (!$userId) {
|
||||||
throw new \RuntimeException('Bitte einloggen, um Themen zu erstellen.');
|
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)) {
|
if (!$access->canCreateThread((int)$userId)) {
|
||||||
$state = $access->getRestrictionState((int)$userId);
|
$state = $access->getRestrictionState((int)$userId);
|
||||||
throw new \RuntimeException($state['reason'] ?: 'Du darfst aktuell keine Themen erstellen.');
|
throw new \RuntimeException($state['reason'] ?: 'Du darfst aktuell keine Themen erstellen.');
|
||||||
@@ -67,8 +73,10 @@ $threads = $community
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="forum-hero__cta">
|
<div class="forum-hero__cta">
|
||||||
<?php if ($userId): ?>
|
<?php if ($userId && !$forumMaintenanceMode): ?>
|
||||||
<button class="btn" type="button" data-modal-open="modalThread">Neues Thema</button>
|
<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: ?>
|
<?php else: ?>
|
||||||
<a class="btn" href="/login">Einloggen und schreiben</a>
|
<a class="btn" href="/login">Einloggen und schreiben</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -82,6 +90,11 @@ $threads = $community
|
|||||||
<?php if ($info): ?>
|
<?php if ($info): ?>
|
||||||
<div class="toast-bar" style="margin-top:14px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
<div class="toast-bar" style="margin-top:14px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
||||||
<?php endif; ?>
|
<?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): ?>
|
<?php if ($selectedBoard): ?>
|
||||||
<div class="forum-shell">
|
<div class="forum-shell">
|
||||||
@@ -239,7 +252,7 @@ $threads = $community
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<?php if ($userId): ?>
|
<?php if ($userId && !$forumMaintenanceMode): ?>
|
||||||
<div class="modal" id="modalThread">
|
<div class="modal" id="modalThread">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="head flex between center-y">
|
<div class="head flex between center-y">
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ $editPostId = (int)($_GET['edit_post'] ?? 0);
|
|||||||
$communityCfg = require __DIR__ . '/../../../config/community.php';
|
$communityCfg = require __DIR__ . '/../../../config/community.php';
|
||||||
$community = $pdo ? new \App\Community($pdo, $communityCfg) : null;
|
$community = $pdo ? new \App\Community($pdo, $communityCfg) : null;
|
||||||
$access = $pdo ? new \App\CommunityAccess($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() : [];
|
$forumCategories = $community ? $community->listForumCategories() : [];
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $community && $access) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $community && $access) {
|
||||||
@@ -21,6 +23,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $community && $access) {
|
|||||||
if (!$userId) {
|
if (!$userId) {
|
||||||
throw new \RuntimeException('Bitte einloggen, um zu antworten.');
|
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)) {
|
if (!$access->canReply((int)$userId)) {
|
||||||
$state = $access->getRestrictionState((int)$userId);
|
$state = $access->getRestrictionState((int)$userId);
|
||||||
throw new \RuntimeException($state['reason'] ?: 'Du darfst aktuell nicht antworten.');
|
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): ?>
|
<?php if ($info): ?>
|
||||||
<div class="toast-bar" style="margin-bottom:14px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
<div class="toast-bar" style="margin-bottom:14px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
||||||
<?php endif; ?>
|
<?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">
|
<article class="forum-post forum-post--lead">
|
||||||
<aside class="forum-post__author">
|
<aside class="forum-post__author">
|
||||||
@@ -290,7 +300,7 @@ $userVotes = ($access && $userId) ? $access->getUserPostVotes((int)$userId, $pos
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($userId): ?>
|
<?php if ($userId && !$forumMaintenanceMode): ?>
|
||||||
<?php if ($userRestrictions['reply_blocked']): ?>
|
<?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>
|
<div class="forum-login-note">Du darfst aktuell keine Antworten schreiben. <?= !empty($userRestrictions['reason']) ? htmlspecialchars((string)$userRestrictions['reason'], ENT_QUOTES) : '' ?></div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@@ -303,6 +313,8 @@ $userVotes = ($access && $userId) ? $access->getUserPostVotes((int)$userId, $pos
|
|||||||
<button class="btn" type="submit">Antwort senden</button>
|
<button class="btn" type="submit">Antwort senden</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php elseif ($userId): ?>
|
||||||
|
<div class="forum-login-note">Die Community ist aktuell im Wartungsmodus. Antworten sind vorübergehend deaktiviert.</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="forum-login-note">Bitte <a href="/login">einloggen</a>, um zu antworten.</div>
|
<div class="forum-login-note">Bitte <a href="/login">einloggen</a>, um zu antworten.</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ if (!in_array($childGender, ['male', 'female', 'mixed'], true)) {
|
|||||||
$debugEnabled = defined('APP_DEBUG') && APP_DEBUG === true;
|
$debugEnabled = defined('APP_DEBUG') && APP_DEBUG === true;
|
||||||
$locationTrackingPreference = 'prompt';
|
$locationTrackingPreference = 'prompt';
|
||||||
$showDebugTools = false;
|
$showDebugTools = false;
|
||||||
|
$adminSystemNotice = null;
|
||||||
if (isset($_SESSION['user_id'])) {
|
if (isset($_SESSION['user_id'])) {
|
||||||
try {
|
try {
|
||||||
$pdo = $app->pdo();
|
$pdo = $app->pdo();
|
||||||
@@ -25,10 +26,30 @@ if (isset($_SESSION['user_id'])) {
|
|||||||
$communityConfig = file_exists($communityCfg) ? require $communityCfg : [];
|
$communityConfig = file_exists($communityCfg) ? require $communityCfg : [];
|
||||||
$communityAccess = new \App\CommunityAccess($pdo, $communityConfig);
|
$communityAccess = new \App\CommunityAccess($pdo, $communityConfig);
|
||||||
$showDebugTools = $debugEnabled && $communityAccess->hasRole((int)$_SESSION['user_id'], 'site_admin');
|
$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) {
|
} catch (\Throwable) {
|
||||||
$locationTrackingPreference = 'prompt';
|
$locationTrackingPreference = 'prompt';
|
||||||
$showDebugTools = false;
|
$showDebugTools = false;
|
||||||
|
$adminSystemNotice = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$debugFiles = [];
|
$debugFiles = [];
|
||||||
@@ -105,6 +126,11 @@ if ($debugEnabled) {
|
|||||||
</script>
|
</script>
|
||||||
<?php tpl('matomo', 'structure'); ?>
|
<?php tpl('matomo', 'structure'); ?>
|
||||||
<?php tpl('nav', '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" id="cookieConsentBanner" hidden>
|
||||||
<div class="cookie-consent__inner">
|
<div class="cookie-consent__inner">
|
||||||
<div class="cookie-consent__copy">
|
<div class="cookie-consent__copy">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ $isLoggedIn = isset($_SESSION['user_id']);
|
|||||||
$displayName = 'Profil';
|
$displayName = 'Profil';
|
||||||
$profileInitial = 'P';
|
$profileInitial = 'P';
|
||||||
$showAdminLink = false;
|
$showAdminLink = false;
|
||||||
|
$showSystemLink = false;
|
||||||
|
|
||||||
if ($isLoggedIn) {
|
if ($isLoggedIn) {
|
||||||
try {
|
try {
|
||||||
@@ -25,12 +26,14 @@ if ($isLoggedIn) {
|
|||||||
$communityConfig = require $communityCfg;
|
$communityConfig = require $communityCfg;
|
||||||
$communityAccess = new \App\CommunityAccess($pdo, $communityConfig);
|
$communityAccess = new \App\CommunityAccess($pdo, $communityConfig);
|
||||||
$showAdminLink = $communityAccess->canModerateForum((int)$_SESSION['user_id']);
|
$showAdminLink = $communityAccess->canModerateForum((int)$_SESSION['user_id']);
|
||||||
|
$showSystemLink = $communityAccess->canManageApplications((int)$_SESSION['user_id']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
$displayName = 'Profil';
|
$displayName = 'Profil';
|
||||||
$profileInitial = 'P';
|
$profileInitial = 'P';
|
||||||
$showAdminLink = false;
|
$showAdminLink = false;
|
||||||
|
$showSystemLink = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -61,6 +64,9 @@ if ($isLoggedIn) {
|
|||||||
<a href="/dashboard?section=events" role="menuitem">Events</a>
|
<a href="/dashboard?section=events" role="menuitem">Events</a>
|
||||||
<a href="/dashboard?section=community" role="menuitem">Community</a>
|
<a href="/dashboard?section=community" role="menuitem">Community</a>
|
||||||
<a href="/dashboard?section=settings" role="menuitem">Einstellungen</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): ?>
|
<?php if ($showAdminLink): ?>
|
||||||
<a href="/community-admin" role="menuitem">Admin</a>
|
<a href="/community-admin" role="menuitem">Admin</a>
|
||||||
<?php endif; ?>
|
<?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=events">Events</a>
|
||||||
<a class="btn ghost" href="/dashboard?section=community">Community</a>
|
<a class="btn ghost" href="/dashboard?section=community">Community</a>
|
||||||
<a class="btn ghost" href="/dashboard?section=settings">Einstellungen</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): ?>
|
<?php if ($showAdminLink): ?>
|
||||||
<a class="btn ghost" href="/community-admin">Admin</a>
|
<a class="btn ghost" href="/community-admin">Admin</a>
|
||||||
<?php endif; ?>
|
<?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
|
CONSTRAINT fk_ep_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) 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
|
-- Community / Forum
|
||||||
CREATE TABLE forum_categories (
|
CREATE TABLE forum_categories (
|
||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|||||||
@@ -174,8 +174,21 @@ final class AccountPages
|
|||||||
$communityAccess = $pdo ? new CommunityAccess($pdo, $communityConfig) : null;
|
$communityAccess = $pdo ? new CommunityAccess($pdo, $communityConfig) : null;
|
||||||
$communityMigration = $pdo ? new CommunityMigration($pdo) : null;
|
$communityMigration = $pdo ? new CommunityMigration($pdo) : null;
|
||||||
$profileSettings = $pdo ? new ProfileSettings($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');
|
$section = (string)($_GET['section'] ?? 'profile');
|
||||||
|
$canManageSystemSettings = $communityAccess ? $communityAccess->canManageApplications($userId) : false;
|
||||||
$allowedSections = ['profile', 'children', 'events', 'community', 'settings'];
|
$allowedSections = ['profile', 'children', 'events', 'community', 'settings'];
|
||||||
|
if ($canManageSystemSettings) {
|
||||||
|
$allowedSections[] = 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($systemSettings) {
|
||||||
|
$systemSettings->ensureSchema();
|
||||||
|
}
|
||||||
|
if ($listingCatalog) {
|
||||||
|
$listingCatalog->ensureSchema();
|
||||||
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
@@ -273,6 +286,26 @@ final class AccountPages
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
$info = 'Einstellungen gespeichert.';
|
$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') {
|
} elseif ($action === 'child_add' || $action === 'child_update') {
|
||||||
$crypto = self::requireCrypto($crypto, 'Kinder');
|
$crypto = self::requireCrypto($crypto, 'Kinder');
|
||||||
$childId = (int)($_POST['child_id'] ?? 0);
|
$childId = (int)($_POST['child_id'] ?? 0);
|
||||||
@@ -588,6 +621,8 @@ final class AccountPages
|
|||||||
'reply_blocked' => false,
|
'reply_blocked' => false,
|
||||||
'reason' => null,
|
'reason' => null,
|
||||||
];
|
];
|
||||||
|
$systemSettingsValues = $systemSettings ? $systemSettings->getAll() : [];
|
||||||
|
$listingCatalogStatus = $listingCatalog ? $listingCatalog->status() : ['complete' => false, 'missing' => [], 'tables' => []];
|
||||||
if (!in_array($section, $allowedSections, true)) {
|
if (!in_array($section, $allowedSections, true)) {
|
||||||
$section = 'profile';
|
$section = 'profile';
|
||||||
}
|
}
|
||||||
@@ -609,6 +644,9 @@ final class AccountPages
|
|||||||
'communityApplication',
|
'communityApplication',
|
||||||
'communityCanApply',
|
'communityCanApply',
|
||||||
'communityRestrictions',
|
'communityRestrictions',
|
||||||
|
'canManageSystemSettings',
|
||||||
|
'systemSettingsValues',
|
||||||
|
'listingCatalogStatus',
|
||||||
'avatarBuilder',
|
'avatarBuilder',
|
||||||
'section',
|
'section',
|
||||||
'allowedSections'
|
'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