This commit is contained in:
@@ -82,6 +82,8 @@ CREATE TABLE events (
|
|||||||
category_slug VARCHAR(120) NULL,
|
category_slug VARCHAR(120) NULL,
|
||||||
image_path VARCHAR(255) NULL,
|
image_path VARCHAR(255) NULL,
|
||||||
location_label VARCHAR(180) NULL,
|
location_label VARCHAR(180) NULL,
|
||||||
|
location_source_type ENUM('custom','place','editorial_event') NOT NULL DEFAULT 'custom',
|
||||||
|
location_source_listing_id BIGINT UNSIGNED NULL,
|
||||||
street VARCHAR(180) NULL,
|
street VARCHAR(180) NULL,
|
||||||
zip CHAR(5) NULL,
|
zip CHAR(5) NULL,
|
||||||
city VARCHAR(120) NULL,
|
city VARCHAR(120) NULL,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
|||||||
- Der Bereich `Events` ist wieder auf echte eigene Events und Event-Teilnahmen begrenzt.
|
- Der Bereich `Events` ist wieder auf echte eigene Events und Event-Teilnahmen begrenzt.
|
||||||
- Der Bereich `Events` bietet zusätzlich einen manuellen ICS-Export und einen persönlichen abonnierbaren Kalender-Feed für alle eigenen Events und Event-Teilnahmen.
|
- Der Bereich `Events` bietet zusätzlich einen manuellen ICS-Export und einen persönlichen abonnierbaren Kalender-Feed für alle eigenen Events und Event-Teilnahmen.
|
||||||
- Der Bereich `Events` ist im UI in `Meine Events`, `Kalendersynchronisation` und `Abgelaufene Events` getrennt, damit aktive Events, Kalender-Themen und Vergangenes sauber getrennt bleiben.
|
- Der Bereich `Events` ist im UI in `Meine Events`, `Kalendersynchronisation` und `Abgelaufene Events` getrennt, damit aktive Events, Kalender-Themen und Vergangenes sauber getrennt bleiben.
|
||||||
- In `Meine Events` sitzt der Button `Neues Event anlegen` direkt in der Box `Eigene Events`; die bisherige Teilnahme-Box heißt jetzt `Angemeldete Events` und enthält zusätzlich einen direkten Link zur `Event Suche`.
|
- In `Meine Events` sitzt der Button `Neues Event anlegen` jetzt direkt in der oberen Tab-Zeile; die bisherige Teilnahme-Box heißt `Angemeldete Events` und enthält zusätzlich einen direkten Link zur `Event Suche`.
|
||||||
- Orte und Veranstaltungen sind vorerst in einen separaten Mitgliederbereichspunkt `Orte & Veranstaltungen` verschoben.
|
- Orte und Veranstaltungen sind vorerst in einen separaten Mitgliederbereichspunkt `Orte & Veranstaltungen` verschoben.
|
||||||
- In `Orte & Veranstaltungen` können Nutzer dauerhafte Orte und zeitlich begrenzte Veranstaltungen neu anlegen.
|
- In `Orte & Veranstaltungen` können Nutzer dauerhafte Orte und zeitlich begrenzte Veranstaltungen neu anlegen.
|
||||||
- Neue Orte und Veranstaltungen bleiben bis zur Freigabe im Status `wartet auf Freigabe` und werden erst danach systemweit veröffentlicht.
|
- Neue Orte und Veranstaltungen bleiben bis zur Freigabe im Status `wartet auf Freigabe` und werden erst danach systemweit veröffentlicht.
|
||||||
@@ -32,8 +32,12 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
|||||||
- Vorschläge für neue Orte und Veranstaltungen dürfen schon vor der Freigabe als Ortsvorschlag für Events genutzt werden, solange sie noch offen geprüft werden.
|
- Vorschläge für neue Orte und Veranstaltungen dürfen schon vor der Freigabe als Ortsvorschlag für Events genutzt werden, solange sie noch offen geprüft werden.
|
||||||
- Dubletten bei Orten und Veranstaltungen sollen serverseitig mindestens über Namen und Adresse abgefangen werden.
|
- Dubletten bei Orten und Veranstaltungen sollen serverseitig mindestens über Namen und Adresse abgefangen werden.
|
||||||
- Die Eingabe im Bereich `Events` soll so einfach wie möglich bleiben und zeigt deshalb nur die für echte Termine relevanten Felder.
|
- Die Eingabe im Bereich `Events` soll so einfach wie möglich bleiben und zeigt deshalb nur die für echte Termine relevanten Felder.
|
||||||
|
- Eigene Events nutzen jetzt als Pflichtfelder nur noch Titel, Datum und `Mit Kindern`; die Beschreibung ist optional.
|
||||||
|
- Für die Event-Location gibt es jetzt die Auswahl `Ort aus Datenbank`, `Veranstaltung aus Datenbank` oder `Benutzerdefiniert`.
|
||||||
|
- Bei `Ort` oder `Veranstaltung` werden Adresse und Kategorie direkt aus dem gewählten Datenbank-Eintrag übernommen.
|
||||||
|
- Bei `Veranstaltung aus Datenbank` muss das Event-Datum innerhalb des hinterlegten Gültigkeitszeitraums dieser Veranstaltungs-Quelle liegen.
|
||||||
- Kategorien werden im Mitgliederbereich per Sucheingabe mit bestehenden Vorschlägen und automatischer Neuanlage gepflegt.
|
- Kategorien werden im Mitgliederbereich per Sucheingabe mit bestehenden Vorschlägen und automatischer Neuanlage gepflegt.
|
||||||
- Eigene Events unterstützen Kategorie, optionales Bild, Ja/Nein-Angabe `Mit Kindern`, optionale Platzzahl und direkte Karten-/Adress-Synchronisierung.
|
- Eigene Events unterstützen optionalen Beschreibungstext, Ja/Nein-Angabe `Mit Kindern`, optionale Platzzahl, optionales Bild sowie eine Location aus Datenbank oder benutzerdefinierte Adresse.
|
||||||
|
|
||||||
## Technischer Rahmen
|
## Technischer Rahmen
|
||||||
- eigener Front-Controller in `public/index.php`
|
- eigener Front-Controller in `public/index.php`
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Stand: 2026-08-10
|
|||||||
- Avatar-Verwaltung: `src/App/Avatar/AvatarManager.php`
|
- Avatar-Verwaltung: `src/App/Avatar/AvatarManager.php`
|
||||||
- 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-/Ausflugsziel-Logik im Mitgliederbereich: `partials/landing/account/dashboard.php`
|
- Profil-/Kinder-/Event-/Ausflugsziel-Logik im Mitgliederbereich: `partials/landing/account/dashboard.php`
|
||||||
|
- Event-Quellen aus Datenbank plus Speicherung der Event-Location-Herkunft: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `Internal/db/schema.sql`
|
||||||
- Consent- und rechtliche Texte: `public/page/datenschutz.php`
|
- Consent- und rechtliche Texte: `public/page/datenschutz.php`
|
||||||
- System-Einstellungen: `src/App/SystemSettings.php`
|
- System-Einstellungen: `src/App/SystemSettings.php`
|
||||||
- neue Listing-/Ort-Basis: `src/App/ListingCatalog.php`
|
- neue Listing-/Ort-Basis: `src/App/ListingCatalog.php`
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
|
|||||||
- System-Sektion für Seiten-Admins mit Wartungs- und Diensteschaltern
|
- System-Sektion für Seiten-Admins mit Wartungs- und Diensteschaltern
|
||||||
- neue Datenbasis für ein späteres Termin-, Ort- und Veranstaltungssystem angelegt
|
- neue Datenbasis für ein späteres Termin-, Ort- und Veranstaltungssystem angelegt
|
||||||
- Mitgliederbereich `Events` ist wieder auf echte eigene Events und Event-Teilnahmen fokussiert
|
- Mitgliederbereich `Events` ist wieder auf echte eigene Events und Event-Teilnahmen fokussiert
|
||||||
- Eigene Events haben jetzt Kategorie, optionales Bild, klare Kinderangabe, optionale Platzzahl und eine direktere Karten-/Adress-Synchronisierung
|
- Eigene Events haben jetzt optionalen Beschreibungstext, klare Kinderangabe, optionale Platzzahl, optionales Bild sowie eine Location aus Datenbank oder benutzerdefinierte Adresse
|
||||||
- Orte und sonstige Veranstaltungen wurden vorerst in einen separaten Bereich `Orte & Veranstaltungen` verschoben
|
- Orte und sonstige Veranstaltungen wurden vorerst in einen separaten Bereich `Orte & Veranstaltungen` verschoben
|
||||||
- Neue Orte und Veranstaltungen müssen vor Veröffentlichung erst durch einen Admin freigegeben werden; Änderungs- und Löschwünsche laufen ebenfalls nur als begründete Anfrage
|
- Neue Orte und Veranstaltungen müssen vor Veröffentlichung erst durch einen Admin freigegeben werden; Änderungs- und Löschwünsche laufen ebenfalls nur als begründete Anfrage
|
||||||
- Die Eingabemasken sind jetzt progressiv aufgebaut: erst Pflichtangaben, optionale Daten in einklappbaren Bereichen
|
- Die Eingabemasken sind jetzt progressiv aufgebaut: erst Pflichtangaben, optionale Daten in einklappbaren Bereichen
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
|
|||||||
- The `Events` area is limited again to real own events and event participations.
|
- The `Events` area is limited again to real own events and event participations.
|
||||||
- The `Events` area now also provides a manual ICS export and a personal subscribable calendar feed for all own events and event participations.
|
- The `Events` area now also provides a manual ICS export and a personal subscribable calendar feed for all own events and event participations.
|
||||||
- The `Events` area is now separated in the UI into `My Events`, `Calendar Sync`, and `Expired Events` so that active events, calendar tasks, and past items stay clearly separated.
|
- The `Events` area is now separated in the UI into `My Events`, `Calendar Sync`, and `Expired Events` so that active events, calendar tasks, and past items stay clearly separated.
|
||||||
- Inside `My Events`, the `Create New Event` button now lives directly in the `Own Events` box; the previous participation box is now labeled `Registered Events` and also includes a direct link to `Event Search`.
|
- Inside `My Events`, the `Create New Event` button now lives directly in the upper tab row; the previous participation box is now labeled `Registered Events` and also includes a direct link to `Event Search`.
|
||||||
- Places and event-like entries were moved for now into a separate member-area section `Places & Events`.
|
- Places and event-like entries were moved for now into a separate member-area section `Places & Events`.
|
||||||
- Inside `Places & Events`, users can create permanent places and time-limited event-style entries.
|
- Inside `Places & Events`, users can create permanent places and time-limited event-style entries.
|
||||||
- New places and event-style entries stay in a pending state until an admin approves them for publication.
|
- New places and event-style entries stay in a pending state until an admin approves them for publication.
|
||||||
@@ -32,8 +32,12 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
|
|||||||
- Pending new places and event-style entries can already be used as location suggestions for events while they are still under review.
|
- Pending new places and event-style entries can already be used as location suggestions for events while they are still under review.
|
||||||
- Duplicate submissions for places and event-style entries should be blocked server-side at least by name and address.
|
- Duplicate submissions for places and event-style entries should be blocked server-side at least by name and address.
|
||||||
- The `Events` input flow should stay as simple as possible and therefore only shows fields relevant for real scheduled events.
|
- The `Events` input flow should stay as simple as possible and therefore only shows fields relevant for real scheduled events.
|
||||||
|
- Own events now only require title, date, and the `with children` choice; the description is optional.
|
||||||
|
- Event locations now support the modes `place from database`, `event from database`, or `custom`.
|
||||||
|
- When a database-backed place or event is selected, the address and category are taken directly from that source entry.
|
||||||
|
- When `event from database` is selected, the event date must fall inside that source event's valid date window.
|
||||||
- Categories in the member area are handled via a search input with existing suggestions and automatic creation when needed.
|
- Categories in the member area are handled via a search input with existing suggestions and automatic creation when needed.
|
||||||
- Own events support category, optional image, a clear `with children` yes/no field, optional capacity, and direct map/address synchronization.
|
- Own events support an optional description, a clear `with children` yes/no field, optional capacity, an optional image, and either a database-backed location or a custom address.
|
||||||
|
|
||||||
## Technical Frame
|
## Technical Frame
|
||||||
- custom front controller in `public/index.php`
|
- custom front controller in `public/index.php`
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Updated: 2026-08-10
|
|||||||
- avatar management: `src/App/Avatar/AvatarManager.php`
|
- avatar management: `src/App/Avatar/AvatarManager.php`
|
||||||
- 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/outing logic inside member area: `partials/landing/account/dashboard.php`
|
- profile/children/event/outing logic inside member area: `partials/landing/account/dashboard.php`
|
||||||
|
- database-backed event location sources plus persisted event location origin: `partials/landing/account/dashboard.php`, `src/App/AccountPages.php`, `Internal/db/schema.sql`
|
||||||
- consent and legal texts: `public/page/datenschutz.php`
|
- consent and legal texts: `public/page/datenschutz.php`
|
||||||
- system settings: `src/App/SystemSettings.php`
|
- system settings: `src/App/SystemSettings.php`
|
||||||
- new listing/place base: `src/App/ListingCatalog.php`
|
- new listing/place base: `src/App/ListingCatalog.php`
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers focused on local events, app
|
|||||||
- system section for site admins with maintenance and service toggles
|
- system section for site admins with maintenance and service toggles
|
||||||
- new data foundation prepared for a broader event, place, and listing system
|
- new data foundation prepared for a broader event, place, and listing system
|
||||||
- member area `Events` is now limited again to real own events and event participations
|
- member area `Events` is now limited again to real own events and event participations
|
||||||
- own events now include category, optional image, explicit child suitability, optional capacity, and more direct map/address synchronization
|
- own events now support an optional description, explicit child suitability, optional capacity, an optional image, and either a database-backed location or a custom address
|
||||||
- places and other event-style entries were moved for now into a separate member-area section `Places & Events`
|
- places and other event-style entries were moved for now into a separate member-area section `Places & Events`
|
||||||
- new places and event-style entries must be approved by an admin before publication; change and deletion wishes also run as reasoned requests
|
- new places and event-style entries must be approved by an admin before publication; change and deletion wishes also run as reasoned requests
|
||||||
- the member-area entry forms now follow a progressive approach: required fields first, optional data inside collapsible sections
|
- the member-area entry forms now follow a progressive approach: required fields first, optional data inside collapsible sections
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf:
|
|||||||
- Im Mitgliederbereich ist `Events` jetzt wieder auf echte eigene Events und Event-Teilnahmen beschränkt
|
- Im Mitgliederbereich ist `Events` jetzt wieder auf echte eigene Events und Event-Teilnahmen beschränkt
|
||||||
- Orte und sonstige Veranstaltungen laufen jetzt separat unter `Orte & Veranstaltungen`
|
- Orte und sonstige Veranstaltungen laufen jetzt separat unter `Orte & Veranstaltungen`
|
||||||
- Neue Orte und Veranstaltungen müssen vor Veröffentlichung erst durch einen Admin freigegeben werden; Änderungs- und Löschwünsche laufen ebenfalls als begründete Anfrage
|
- Neue Orte und Veranstaltungen müssen vor Veröffentlichung erst durch einen Admin freigegeben werden; Änderungs- und Löschwünsche laufen ebenfalls als begründete Anfrage
|
||||||
- Eigene Events unterstützen aktuell Kategorie, Bild-Upload, klare Kinderangabe, optionale Platzzahl und direkte Karten-/Adress-Synchronisierung
|
- Eigene Events unterstützen aktuell optionalen Beschreibungstext, klare Kinderangabe, optionale Platzzahl, Bild-Upload sowie eine Location aus Datenbank oder benutzerdefinierte Adresse
|
||||||
- Die Eingabe im Mitgliederbereich ist jetzt bewusst vereinfacht: zuerst nur Pflichtangaben, optionale Angaben in einklappbaren Bereichen
|
- Die Eingabe im Mitgliederbereich ist jetzt bewusst vereinfacht: zuerst nur Pflichtangaben, optionale Angaben in einklappbaren Bereichen
|
||||||
- Kategorien werden jetzt über eine Sucheingabe mit bestehenden Vorschlägen gepflegt; neue Kategorien werden automatisch angelegt und sind für Seiten-Admins im Systembereich zusammenführbar
|
- Kategorien werden jetzt über eine Sucheingabe mit bestehenden Vorschlägen gepflegt; neue Kategorien werden automatisch angelegt und sind für Seiten-Admins im Systembereich zusammenführbar
|
||||||
- Im Mitgliederbereich `Events` gibt es jetzt zusätzlich einen ICS-Download und einen persönlichen abonnierbaren Kalender-Feed für alle eigenen Events und Event-Teilnahmen
|
- Im Mitgliederbereich `Events` gibt es jetzt zusätzlich einen ICS-Download und einen persönlichen abonnierbaren Kalender-Feed für alle eigenen Events und Event-Teilnahmen
|
||||||
|
|||||||
@@ -54,6 +54,31 @@ foreach (($listingCategories ?? []) as $categoryRow) {
|
|||||||
$categoryTitleMap[(string)($categoryRow['slug'] ?? '')] = (string)($categoryRow['title'] ?? '');
|
$categoryTitleMap[(string)($categoryRow['slug'] ?? '')] = (string)($categoryRow['title'] ?? '');
|
||||||
}
|
}
|
||||||
$categoryInputValue = $categoryTitleMap[$categorySlugValue] ?? $categorySlugValue;
|
$categoryInputValue = $categoryTitleMap[$categorySlugValue] ?? $categorySlugValue;
|
||||||
|
$eventLocationModeValue = $editingEvent ? (string)($editEvent['location_source_type'] ?? 'custom') : 'custom';
|
||||||
|
$eventLocationSourceIdValue = $editingEvent && !empty($editEvent['location_source_listing_id']) ? (int)$editEvent['location_source_listing_id'] : 0;
|
||||||
|
$eventLocationSourceInputValue = '';
|
||||||
|
foreach (($eventLocationOptions ?? []) as $locationOption) {
|
||||||
|
if ((int)($locationOption['id'] ?? 0) !== $eventLocationSourceIdValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$optionTypeLabel = (string)($locationOption['listing_type'] ?? '') === 'place' ? 'Ort' : 'Veranstaltung';
|
||||||
|
$optionAddressLabel = trim(implode(', ', array_filter([
|
||||||
|
(string)($locationOption['street'] ?? ''),
|
||||||
|
trim((string)($locationOption['zip'] ?? '') . ' ' . (string)($locationOption['city'] ?? '')),
|
||||||
|
])));
|
||||||
|
$optionDateLabel = !empty($locationOption['starts_at']) ? date('d.m.Y', strtotime((string)$locationOption['starts_at'])) : '';
|
||||||
|
$eventLocationSourceInputValue = trim(implode(' · ', array_filter([
|
||||||
|
$optionTypeLabel,
|
||||||
|
(string)($locationOption['title'] ?? ''),
|
||||||
|
$optionDateLabel,
|
||||||
|
$optionAddressLabel,
|
||||||
|
])));
|
||||||
|
if ($eventLocationModeValue !== 'custom' && !empty($locationOption['category_title'])) {
|
||||||
|
$categoryInputValue = (string)$locationOption['category_title'];
|
||||||
|
$categorySlugValue = (string)($locationOption['category_slug'] ?? $categorySlugValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
$priceRows = [];
|
$priceRows = [];
|
||||||
if ($editingListing && !empty($editListing['prices']) && is_array($editListing['prices'])) {
|
if ($editingListing && !empty($editListing['prices']) && is_array($editListing['prices'])) {
|
||||||
foreach ($editListing['prices'] as $priceRow) {
|
foreach ($editListing['prices'] as $priceRow) {
|
||||||
@@ -1100,6 +1125,7 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
<?php if ($editingListing && $editListing): ?>
|
<?php if ($editingListing && $editListing): ?>
|
||||||
<input type="hidden" name="listing_id" value="<?= (int)$editListing['id'] ?>">
|
<input type="hidden" name="listing_id" value="<?= (int)$editListing['id'] ?>">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<input type="hidden" name="event_location_source_id" id="eventLocationSourceId" value="<?= $eventLocationSourceIdValue > 0 ? (int)$eventLocationSourceIdValue : '' ?>">
|
||||||
<datalist id="existingPlacesList">
|
<datalist id="existingPlacesList">
|
||||||
<?php foreach ($placeSuggestions as $placeSuggestion): ?>
|
<?php foreach ($placeSuggestions as $placeSuggestion): ?>
|
||||||
<option value="<?= htmlspecialchars((string)$placeSuggestion['title'], ENT_QUOTES) ?>" data-place='<?= htmlspecialchars(json_encode($placeSuggestion, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES) ?>'>
|
<option value="<?= htmlspecialchars((string)$placeSuggestion['title'], ENT_QUOTES) ?>" data-place='<?= htmlspecialchars(json_encode($placeSuggestion, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES) ?>'>
|
||||||
@@ -1107,6 +1133,25 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</datalist>
|
</datalist>
|
||||||
|
<datalist id="eventLocationOptionList">
|
||||||
|
<?php foreach (($eventLocationOptions ?? []) as $locationOption): ?>
|
||||||
|
<?php
|
||||||
|
$optionTypeLabel = (string)($locationOption['listing_type'] ?? '') === 'place' ? 'Ort' : 'Veranstaltung';
|
||||||
|
$optionAddressLabel = trim(implode(', ', array_filter([
|
||||||
|
(string)($locationOption['street'] ?? ''),
|
||||||
|
trim((string)($locationOption['zip'] ?? '') . ' ' . (string)($locationOption['city'] ?? '')),
|
||||||
|
])));
|
||||||
|
$optionDateLabel = !empty($locationOption['starts_at']) ? date('d.m.Y', strtotime((string)$locationOption['starts_at'])) : '';
|
||||||
|
$optionLabel = trim(implode(' · ', array_filter([
|
||||||
|
$optionTypeLabel,
|
||||||
|
(string)($locationOption['title'] ?? ''),
|
||||||
|
$optionDateLabel,
|
||||||
|
$optionAddressLabel,
|
||||||
|
])));
|
||||||
|
?>
|
||||||
|
<option value="<?= htmlspecialchars($optionLabel, ENT_QUOTES) ?>" data-location='<?= htmlspecialchars(json_encode($locationOption, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES) ?>'></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</datalist>
|
||||||
<?php if (!$editing && $showEventTypeChoice): ?>
|
<?php if (!$editing && $showEventTypeChoice): ?>
|
||||||
<div class="stack gap-8">
|
<div class="stack gap-8">
|
||||||
<label class="label">Was möchtest du anlegen?</label>
|
<label class="label">Was möchtest du anlegen?</label>
|
||||||
@@ -1130,8 +1175,19 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evDesc">Beschreibung</label>
|
<label class="label" for="evDesc">Beschreibung</label>
|
||||||
<textarea id="evDesc" name="description" class="textarea" rows="4" placeholder="Was erwartet Familien vor Ort?" required><?= htmlspecialchars($descriptionValue, ENT_QUOTES) ?></textarea>
|
<textarea id="evDesc" name="description" class="textarea" rows="4" placeholder="Was erwartet Familien vor Ort?"><?= htmlspecialchars($descriptionValue, ENT_QUOTES) ?></textarea>
|
||||||
<p class="muted small" style="margin:0;">Die Kurzbeschreibung wird automatisch aus dem Text erzeugt.</p>
|
</div>
|
||||||
|
<div class="stack gap-6" id="eventLocationModeWrap">
|
||||||
|
<label class="label" for="eventLocationMode">Location</label>
|
||||||
|
<select id="eventLocationMode" name="event_location_mode" class="select">
|
||||||
|
<option value="place" <?= $eventLocationModeValue === 'place' ? 'selected' : '' ?>>Ort aus Datenbank</option>
|
||||||
|
<option value="editorial_event" <?= $eventLocationModeValue === 'editorial_event' ? 'selected' : '' ?>>Veranstaltung aus Datenbank</option>
|
||||||
|
<option value="custom" <?= $eventLocationModeValue === 'custom' ? 'selected' : '' ?>>Benutzerdefiniert</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6" id="eventLocationSourceWrap">
|
||||||
|
<label class="label" for="eventLocationSourceInput" id="eventLocationSourceLabel">Ort aus Datenbank</label>
|
||||||
|
<input id="eventLocationSourceInput" class="input" list="eventLocationOptionList" placeholder="Eintrag aus der Datenbank auswählen" value="<?= htmlspecialchars($eventLocationSourceInputValue, ENT_QUOTES) ?>">
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="categoryInput">Kategorie</label>
|
<label class="label" for="categoryInput">Kategorie</label>
|
||||||
@@ -1184,7 +1240,7 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
<?php elseif (($section ?? '') === 'places'): ?>
|
<?php elseif (($section ?? '') === 'places'): ?>
|
||||||
<p class="muted small" style="margin:0;">Neue Orte und Veranstaltungen werden erst nach Prüfung durch einen Admin freigegeben.</p>
|
<p class="muted small" style="margin:0;">Neue Orte und Veranstaltungen werden erst nach Prüfung durch einen Admin freigegeben.</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="form-grid">
|
<div class="form-grid" id="eventAddressFieldsWrap">
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evZip">PLZ</label>
|
<label class="label" for="evZip">PLZ</label>
|
||||||
<input id="evZip" name="zip" class="input" maxlength="5" value="<?= htmlspecialchars($zipValue, ENT_QUOTES) ?>">
|
<input id="evZip" name="zip" class="input" maxlength="5" value="<?= htmlspecialchars($zipValue, ENT_QUOTES) ?>">
|
||||||
@@ -1194,18 +1250,17 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
<input id="evCity" name="city" class="input" value="<?= htmlspecialchars($cityValue, ENT_QUOTES) ?>">
|
<input id="evCity" name="city" class="input" value="<?= htmlspecialchars($cityValue, ENT_QUOTES) ?>">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-grid">
|
<div class="form-grid" id="eventAddressStreetWrap">
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evStreet">Straße / Adresse</label>
|
<label class="label" for="evStreet">Straße / Adresse</label>
|
||||||
<input id="evStreet" name="street" class="input" placeholder="z. B. Musterstraße 12" value="<?= htmlspecialchars($streetValue, ENT_QUOTES) ?>">
|
<input id="evStreet" name="street" class="input" placeholder="z. B. Musterstraße 12" value="<?= htmlspecialchars($streetValue, ENT_QUOTES) ?>">
|
||||||
<p class="muted small">Wird zur Karten-/Umkreissuche genutzt.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evRegion">Region/Bezirk</label>
|
<label class="label" for="evRegion">Region/Bezirk</label>
|
||||||
<input id="evRegion" name="region" class="input" value="<?= htmlspecialchars($regionValue, ENT_QUOTES) ?>">
|
<input id="evRegion" name="region" class="input" value="<?= htmlspecialchars($regionValue, ENT_QUOTES) ?>">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-8" style="flex-wrap:wrap; align-items:center;">
|
<div class="flex gap-8" style="flex-wrap:wrap; align-items:center;" id="eventAddressActionsWrap">
|
||||||
<button class="btn ghost" type="button" id="btnEventAddressSearch">Adresse suchen</button>
|
<button class="btn ghost" type="button" id="btnEventAddressSearch">Adresse suchen</button>
|
||||||
<button class="btn ghost" type="button" id="btnEventBrowserLocation">📍 Aktuellen Standort</button>
|
<button class="btn ghost" type="button" id="btnEventBrowserLocation">📍 Aktuellen Standort</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1355,6 +1410,7 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
const btnEventBrowserLocation = document.getElementById('btnEventBrowserLocation');
|
const btnEventBrowserLocation = document.getElementById('btnEventBrowserLocation');
|
||||||
const eventAddressStatus = document.getElementById('eventAddressStatus');
|
const eventAddressStatus = document.getElementById('eventAddressStatus');
|
||||||
const mapSearch = document.getElementById('mapSearch');
|
const mapSearch = document.getElementById('mapSearch');
|
||||||
|
const mapDetails = document.getElementById('mapDetails');
|
||||||
const latInput = document.getElementById('evLat');
|
const latInput = document.getElementById('evLat');
|
||||||
const lngInput = document.getElementById('evLng');
|
const lngInput = document.getElementById('evLng');
|
||||||
const entryKindInput = document.getElementById('entryKind');
|
const entryKindInput = document.getElementById('entryKind');
|
||||||
@@ -1364,6 +1420,15 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
const eventDateLabel = document.getElementById('eventDateLabel');
|
const eventDateLabel = document.getElementById('eventDateLabel');
|
||||||
const eventChildrenWrap = document.getElementById('eventChildrenWrap');
|
const eventChildrenWrap = document.getElementById('eventChildrenWrap');
|
||||||
const eventCapacityWrap = document.getElementById('eventCapacityWrap');
|
const eventCapacityWrap = document.getElementById('eventCapacityWrap');
|
||||||
|
const eventLocationModeWrap = document.getElementById('eventLocationModeWrap');
|
||||||
|
const eventLocationModeSelect = document.getElementById('eventLocationMode');
|
||||||
|
const eventLocationSourceWrap = document.getElementById('eventLocationSourceWrap');
|
||||||
|
const eventLocationSourceInput = document.getElementById('eventLocationSourceInput');
|
||||||
|
const eventLocationSourceIdInput = document.getElementById('eventLocationSourceId');
|
||||||
|
const eventLocationSourceLabel = document.getElementById('eventLocationSourceLabel');
|
||||||
|
const eventAddressFieldsWrap = document.getElementById('eventAddressFieldsWrap');
|
||||||
|
const eventAddressStreetWrap = document.getElementById('eventAddressStreetWrap');
|
||||||
|
const eventAddressActionsWrap = document.getElementById('eventAddressActionsWrap');
|
||||||
const eventValidUntilWrap = document.getElementById('eventValidUntilWrap');
|
const eventValidUntilWrap = document.getElementById('eventValidUntilWrap');
|
||||||
const eventPricingWrap = document.getElementById('eventPricingWrap');
|
const eventPricingWrap = document.getElementById('eventPricingWrap');
|
||||||
const visibilityDetails = document.getElementById('visibilityDetails');
|
const visibilityDetails = document.getElementById('visibilityDetails');
|
||||||
@@ -1371,6 +1436,7 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
const placeOpeningHoursWrap = document.getElementById('placeOpeningHoursWrap');
|
const placeOpeningHoursWrap = document.getElementById('placeOpeningHoursWrap');
|
||||||
const eventRecurrenceWrap = document.getElementById('eventRecurrenceWrap');
|
const eventRecurrenceWrap = document.getElementById('eventRecurrenceWrap');
|
||||||
const eventDateInput = document.getElementById('evDate');
|
const eventDateInput = document.getElementById('evDate');
|
||||||
|
const eventDescriptionInput = document.getElementById('evDesc');
|
||||||
const categoryInput = document.getElementById('categoryInput');
|
const categoryInput = document.getElementById('categoryInput');
|
||||||
const categorySlugInput = document.getElementById('listingCategorySlug');
|
const categorySlugInput = document.getElementById('listingCategorySlug');
|
||||||
const categoryInputStatus = document.getElementById('categoryInputStatus');
|
const categoryInputStatus = document.getElementById('categoryInputStatus');
|
||||||
@@ -1429,6 +1495,77 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findEventLocationOptionByLabel(label) {
|
||||||
|
const normalized = (label || '').trim();
|
||||||
|
if (!normalized) return null;
|
||||||
|
const option = document.querySelector(`#eventLocationOptionList option[value="${CSS.escape(normalized)}"]`);
|
||||||
|
if (!option) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(option.getAttribute('data-location') || 'null');
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEventLocationOption(option) {
|
||||||
|
if (!option) return;
|
||||||
|
if (eventLocationSourceIdInput) eventLocationSourceIdInput.value = String(option.id || '');
|
||||||
|
if (streetInput) streetInput.value = String(option.street || '');
|
||||||
|
if (zipInput) zipInput.value = String(option.zip || '');
|
||||||
|
if (cityInput) cityInput.value = String(option.city || '');
|
||||||
|
if (regionInput) regionInput.value = String(option.region || '');
|
||||||
|
if (latInput) latInput.value = option.lat !== null && option.lat !== '' ? Number(option.lat).toFixed(7) : '';
|
||||||
|
if (lngInput) lngInput.value = option.lng !== null && option.lng !== '' ? Number(option.lng).toFixed(7) : '';
|
||||||
|
if (categoryInput) categoryInput.value = String(option.category_title || '');
|
||||||
|
if (categorySlugInput) categorySlugInput.value = String(option.category_slug || '');
|
||||||
|
if (categoryInputStatus) categoryInputStatus.textContent = '';
|
||||||
|
if (eventAddressStatus) eventAddressStatus.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearEventLocationSelection() {
|
||||||
|
if (eventLocationSourceIdInput) eventLocationSourceIdInput.value = '';
|
||||||
|
if (eventLocationSourceInput) eventLocationSourceInput.value = '';
|
||||||
|
if (categoryInputStatus) categoryInputStatus.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncEventLocationMode() {
|
||||||
|
const kind = entryKindInput?.value || 'event';
|
||||||
|
const mode = eventLocationModeSelect?.value || 'custom';
|
||||||
|
const isOwnEvent = kind === 'event';
|
||||||
|
const usesDatabaseLocation = isOwnEvent && mode !== 'custom';
|
||||||
|
|
||||||
|
setSectionState(eventLocationModeWrap, isOwnEvent);
|
||||||
|
setSectionState(eventLocationSourceWrap, usesDatabaseLocation);
|
||||||
|
setSectionState(eventAddressFieldsWrap, !usesDatabaseLocation);
|
||||||
|
setSectionState(eventAddressStreetWrap, !usesDatabaseLocation);
|
||||||
|
setSectionState(eventAddressActionsWrap, !usesDatabaseLocation);
|
||||||
|
setSectionState(mapDetails, !usesDatabaseLocation);
|
||||||
|
|
||||||
|
if (eventLocationSourceLabel) {
|
||||||
|
eventLocationSourceLabel.textContent = mode === 'editorial_event' ? 'Veranstaltung aus Datenbank' : 'Ort aus Datenbank';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryInput) {
|
||||||
|
categoryInput.readOnly = usesDatabaseLocation;
|
||||||
|
if (usesDatabaseLocation) {
|
||||||
|
categoryInput.removeAttribute('list');
|
||||||
|
} else {
|
||||||
|
categoryInput.setAttribute('list', 'listingCategoryList');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usesDatabaseLocation) {
|
||||||
|
const option = findEventLocationOptionByLabel(eventLocationSourceInput?.value || '');
|
||||||
|
if (option && String(option.listing_type || '') === mode) {
|
||||||
|
applyEventLocationOption(option);
|
||||||
|
} else {
|
||||||
|
clearEventLocationSelection();
|
||||||
|
}
|
||||||
|
} else if (isOwnEvent) {
|
||||||
|
if (eventLocationSourceIdInput) eventLocationSourceIdInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function syncEventEntryKind(nextKind) {
|
function syncEventEntryKind(nextKind) {
|
||||||
const kind = nextKind || entryKindInput?.value || 'event';
|
const kind = nextKind || entryKindInput?.value || 'event';
|
||||||
if (entryKindInput) entryKindInput.value = kind;
|
if (entryKindInput) entryKindInput.value = kind;
|
||||||
@@ -1451,7 +1588,8 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
|
|
||||||
setSectionState(eventDateRow, kind !== 'place');
|
setSectionState(eventDateRow, kind !== 'place');
|
||||||
if (eventDateInput) eventDateInput.required = kind !== 'place';
|
if (eventDateInput) eventDateInput.required = kind !== 'place';
|
||||||
if (categoryInput) categoryInput.required = true;
|
if (eventDescriptionInput) eventDescriptionInput.required = kind !== 'event';
|
||||||
|
if (categoryInput) categoryInput.required = kind !== 'event';
|
||||||
setSectionState(eventChildrenWrap, kind === 'event' || kind === 'editorial_event');
|
setSectionState(eventChildrenWrap, kind === 'event' || kind === 'editorial_event');
|
||||||
setSectionState(eventCapacityWrap, kind === 'event');
|
setSectionState(eventCapacityWrap, kind === 'event');
|
||||||
setSectionState(eventValidUntilWrap, kind === 'editorial_event');
|
setSectionState(eventValidUntilWrap, kind === 'editorial_event');
|
||||||
@@ -1463,6 +1601,7 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
if (eventDateLabel) {
|
if (eventDateLabel) {
|
||||||
eventDateLabel.textContent = kind === 'editorial_event' ? 'Gültig von' : 'Datum';
|
eventDateLabel.textContent = kind === 'editorial_event' ? 'Gültig von' : 'Datum';
|
||||||
}
|
}
|
||||||
|
syncEventLocationMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
entryKindButtons.forEach((button) => {
|
entryKindButtons.forEach((button) => {
|
||||||
@@ -1470,6 +1609,19 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
syncEventEntryKind(button.getAttribute('data-entry-kind-choice') || 'event');
|
syncEventEntryKind(button.getAttribute('data-entry-kind-choice') || 'event');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
eventLocationModeSelect?.addEventListener('change', () => {
|
||||||
|
clearEventLocationSelection();
|
||||||
|
syncEventLocationMode();
|
||||||
|
});
|
||||||
|
eventLocationSourceInput?.addEventListener('change', () => {
|
||||||
|
const mode = eventLocationModeSelect?.value || 'custom';
|
||||||
|
const option = findEventLocationOptionByLabel(eventLocationSourceInput.value || '');
|
||||||
|
if (!option || String(option.listing_type || '') !== mode) {
|
||||||
|
clearEventLocationSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyEventLocationOption(option);
|
||||||
|
});
|
||||||
syncEventEntryKind(entryKindInput?.value || 'event');
|
syncEventEntryKind(entryKindInput?.value || 'event');
|
||||||
|
|
||||||
function createOpeningRow() {
|
function createOpeningRow() {
|
||||||
@@ -1503,8 +1655,12 @@ if (!empty($canManageSystemSettings)) {
|
|||||||
btnAddPriceRow?.addEventListener('click', createPriceRow);
|
btnAddPriceRow?.addEventListener('click', createPriceRow);
|
||||||
|
|
||||||
function syncCategoryInputState() {
|
function syncCategoryInputState() {
|
||||||
const value = (categoryInput?.value || '').trim().toLowerCase();
|
|
||||||
if (!categorySlugInput || !categoryInputStatus) return;
|
if (!categorySlugInput || !categoryInputStatus) return;
|
||||||
|
if ((entryKindInput?.value || 'event') === 'event' && (eventLocationModeSelect?.value || 'custom') !== 'custom') {
|
||||||
|
categoryInputStatus.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value = (categoryInput?.value || '').trim().toLowerCase();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
categorySlugInput.value = '';
|
categorySlugInput.value = '';
|
||||||
categoryInputStatus.textContent = '';
|
categoryInputStatus.textContent = '';
|
||||||
|
|||||||
@@ -454,14 +454,51 @@ final class AccountPages
|
|||||||
$existingEvent = $stmt?->fetch(\PDO::FETCH_ASSOC) ?: null;
|
$existingEvent = $stmt?->fetch(\PDO::FETCH_ASSOC) ?: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$imagePath = self::storeUploadedImage($_FILES['image_file'] ?? null, is_array($existingEvent) ? (string)($existingEvent['image_path'] ?? '') : '');
|
||||||
|
$allowKids = ((string)($_POST['with_children'] ?? 'yes')) === 'yes' ? 1 : 0;
|
||||||
|
$capacity = isset($_POST['max_participants']) && $_POST['max_participants'] !== '' ? (int)$_POST['max_participants'] : null;
|
||||||
|
$eventStartValue = self::normalizeDateForStorage((string)($_POST['starts_at'] ?? ''), true);
|
||||||
|
$sourceMode = (string)($_POST['event_location_mode'] ?? 'custom');
|
||||||
|
$sourceListingId = isset($_POST['event_location_source_id']) && $_POST['event_location_source_id'] !== '' ? (int)$_POST['event_location_source_id'] : null;
|
||||||
|
$locationLabel = null;
|
||||||
$street = trim((string)($_POST['street'] ?? ''));
|
$street = trim((string)($_POST['street'] ?? ''));
|
||||||
$zip = trim((string)($_POST['zip'] ?? ''));
|
$zip = trim((string)($_POST['zip'] ?? ''));
|
||||||
$city = trim((string)($_POST['city'] ?? ''));
|
$city = trim((string)($_POST['city'] ?? ''));
|
||||||
$region = trim((string)($_POST['region'] ?? ''));
|
$region = trim((string)($_POST['region'] ?? ''));
|
||||||
$lat = isset($_POST['lat']) && $_POST['lat'] !== '' ? (float)$_POST['lat'] : null;
|
$lat = isset($_POST['lat']) && $_POST['lat'] !== '' ? (float)$_POST['lat'] : null;
|
||||||
$lng = isset($_POST['lng']) && $_POST['lng'] !== '' ? (float)$_POST['lng'] : null;
|
$lng = isset($_POST['lng']) && $_POST['lng'] !== '' ? (float)$_POST['lng'] : null;
|
||||||
|
$categoryInput = trim((string)($_POST['category_input'] ?? $_POST['category_slug'] ?? ''));
|
||||||
|
$categorySlug = '';
|
||||||
|
$sourceListing = null;
|
||||||
|
if (in_array($sourceMode, ['place', 'editorial_event'], true)) {
|
||||||
|
if (!$listingCatalog || $sourceListingId === null || $sourceListingId <= 0) {
|
||||||
|
throw new \RuntimeException('Bitte wähle einen gültigen Ort oder eine gültige Veranstaltung aus der Datenbank.');
|
||||||
|
}
|
||||||
|
$sourceListing = $listingCatalog->getEventLocationSource($sourceListingId);
|
||||||
|
if (!$sourceListing || (string)($sourceListing['listing_type'] ?? '') !== $sourceMode) {
|
||||||
|
throw new \RuntimeException('Die gewählte Location ist nicht verfügbar.');
|
||||||
|
}
|
||||||
|
if ($sourceMode === 'editorial_event') {
|
||||||
|
$eventDate = substr($eventStartValue, 0, 10);
|
||||||
|
$sourceStart = !empty($sourceListing['starts_at']) ? substr((string)$sourceListing['starts_at'], 0, 10) : '';
|
||||||
|
$sourceUntil = !empty($sourceListing['recurrence_until']) ? substr((string)$sourceListing['recurrence_until'], 0, 10) : '';
|
||||||
|
if ($eventDate === '' || $sourceStart === '' || $sourceUntil === '' || $eventDate < $sourceStart || $eventDate > $sourceUntil) {
|
||||||
|
throw new \RuntimeException('Die gewählte Veranstaltungs-Location passt zeitlich nicht zum Event-Datum.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$street = trim((string)($sourceListing['street'] ?? ''));
|
||||||
|
$zip = trim((string)($sourceListing['zip'] ?? ''));
|
||||||
|
$city = trim((string)($sourceListing['city'] ?? ''));
|
||||||
|
$region = trim((string)($sourceListing['region'] ?? ''));
|
||||||
|
$lat = isset($sourceListing['lat']) && $sourceListing['lat'] !== '' ? (float)$sourceListing['lat'] : null;
|
||||||
|
$lng = isset($sourceListing['lng']) && $sourceListing['lng'] !== '' ? (float)$sourceListing['lng'] : null;
|
||||||
|
$locationLabel = trim((string)($sourceListing['title'] ?? '')) ?: null;
|
||||||
|
$categorySlug = trim((string)($sourceListing['category_slug'] ?? ''));
|
||||||
|
} else {
|
||||||
$hasAddress = $street !== '' || $zip !== '' || $city !== '' || $region !== '';
|
$hasAddress = $street !== '' || $zip !== '' || $city !== '' || $region !== '';
|
||||||
if ($hasAddress) {
|
if (!$hasAddress) {
|
||||||
|
throw new \RuntimeException('Bitte gib für eine benutzerdefinierte Location eine Adresse an.');
|
||||||
|
}
|
||||||
$resolvedAddress = null;
|
$resolvedAddress = null;
|
||||||
if ($lat !== null && $lng !== null) {
|
if ($lat !== null && $lng !== null) {
|
||||||
$resolvedAddress = self::reverseGeocodeAddress($lat, $lng);
|
$resolvedAddress = self::reverseGeocodeAddress($lat, $lng);
|
||||||
@@ -479,21 +516,14 @@ final class AccountPages
|
|||||||
$region = $resolvedAddress['region'] !== '' ? $resolvedAddress['region'] : $region;
|
$region = $resolvedAddress['region'] !== '' ? $resolvedAddress['region'] : $region;
|
||||||
$lat = $resolvedAddress['lat'];
|
$lat = $resolvedAddress['lat'];
|
||||||
$lng = $resolvedAddress['lng'];
|
$lng = $resolvedAddress['lng'];
|
||||||
}
|
|
||||||
$imagePath = self::storeUploadedImage($_FILES['image_file'] ?? null, is_array($existingEvent) ? (string)($existingEvent['image_path'] ?? '') : '');
|
|
||||||
$allowKids = ((string)($_POST['with_children'] ?? 'yes')) === 'yes' ? 1 : 0;
|
|
||||||
$capacity = isset($_POST['max_participants']) && $_POST['max_participants'] !== '' ? (int)$_POST['max_participants'] : null;
|
|
||||||
$categoryInput = trim((string)($_POST['category_input'] ?? $_POST['category_slug'] ?? ''));
|
|
||||||
$categorySlug = '';
|
|
||||||
if ($categoryInput !== '' && $listingCatalog) {
|
if ($categoryInput !== '' && $listingCatalog) {
|
||||||
$category = $listingCatalog->ensureCategoryForInput($categoryInput, 'event');
|
$category = $listingCatalog->ensureCategoryForInput($categoryInput, 'event');
|
||||||
$categorySlug = (string)($category['slug'] ?? '');
|
$categorySlug = (string)($category['slug'] ?? '');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$eventStartValue = self::normalizeDateForStorage((string)($_POST['starts_at'] ?? ''), true);
|
|
||||||
|
|
||||||
if ($action === 'event_add') {
|
if ($action === 'event_add') {
|
||||||
$stmt = $pdo?->prepare('INSERT INTO events (created_by, title, teaser_public, description, category_slug, image_path, location_label, street, zip, city, region, lat, lng, starts_at, max_participants, allow_kids, visibility, status, created_at, updated_at) VALUES (:uid, :title, :teaser, :descr, :categorySlug, :imagePath, :loc, :street, :zip, :city, :region, :lat, :lng, :start, :capacity, :allow, :vis, :status, NOW(), NOW())');
|
$stmt = $pdo?->prepare('INSERT INTO events (created_by, title, teaser_public, description, category_slug, image_path, location_label, location_source_type, location_source_listing_id, street, zip, city, region, lat, lng, starts_at, max_participants, allow_kids, visibility, status, created_at, updated_at) VALUES (:uid, :title, :teaser, :descr, :categorySlug, :imagePath, :loc, :sourceType, :sourceListingId, :street, :zip, :city, :region, :lat, :lng, :start, :capacity, :allow, :vis, :status, NOW(), NOW())');
|
||||||
$stmt?->execute([
|
$stmt?->execute([
|
||||||
'uid' => $userId,
|
'uid' => $userId,
|
||||||
'title' => trim((string)$_POST['title']),
|
'title' => trim((string)$_POST['title']),
|
||||||
@@ -501,7 +531,9 @@ final class AccountPages
|
|||||||
'descr' => trim((string)$_POST['description']),
|
'descr' => trim((string)$_POST['description']),
|
||||||
'categorySlug' => $categorySlug !== '' ? $categorySlug : null,
|
'categorySlug' => $categorySlug !== '' ? $categorySlug : null,
|
||||||
'imagePath' => $imagePath !== '' ? $imagePath : null,
|
'imagePath' => $imagePath !== '' ? $imagePath : null,
|
||||||
'loc' => null,
|
'loc' => $locationLabel,
|
||||||
|
'sourceType' => $sourceMode,
|
||||||
|
'sourceListingId' => $sourceMode === 'custom' ? null : $sourceListingId,
|
||||||
'street' => $street ?: null,
|
'street' => $street ?: null,
|
||||||
'zip' => $zip,
|
'zip' => $zip,
|
||||||
'city' => $city,
|
'city' => $city,
|
||||||
@@ -524,7 +556,7 @@ final class AccountPages
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$eventId = (int)($_POST['event_id'] ?? 0);
|
$eventId = (int)($_POST['event_id'] ?? 0);
|
||||||
$stmt = $pdo?->prepare('UPDATE events SET title=:title, teaser_public=:teaser, description=:descr, category_slug=:categorySlug, image_path=:imagePath, location_label=:loc, street=:street, zip=:zip, city=:city, region=:region, lat=:lat, lng=:lng, starts_at=:start, max_participants=:capacity, allow_kids=:allow, visibility=:vis, updated_at=NOW() WHERE id=:id AND created_by=:uid');
|
$stmt = $pdo?->prepare('UPDATE events SET title=:title, teaser_public=:teaser, description=:descr, category_slug=:categorySlug, image_path=:imagePath, location_label=:loc, location_source_type=:sourceType, location_source_listing_id=:sourceListingId, street=:street, zip=:zip, city=:city, region=:region, lat=:lat, lng=:lng, starts_at=:start, max_participants=:capacity, allow_kids=:allow, visibility=:vis, updated_at=NOW() WHERE id=:id AND created_by=:uid');
|
||||||
$stmt?->execute([
|
$stmt?->execute([
|
||||||
'id' => $eventId,
|
'id' => $eventId,
|
||||||
'uid' => $userId,
|
'uid' => $userId,
|
||||||
@@ -533,7 +565,9 @@ final class AccountPages
|
|||||||
'descr' => trim((string)$_POST['description']),
|
'descr' => trim((string)$_POST['description']),
|
||||||
'categorySlug' => $categorySlug !== '' ? $categorySlug : null,
|
'categorySlug' => $categorySlug !== '' ? $categorySlug : null,
|
||||||
'imagePath' => $imagePath !== '' ? $imagePath : null,
|
'imagePath' => $imagePath !== '' ? $imagePath : null,
|
||||||
'loc' => null,
|
'loc' => $locationLabel,
|
||||||
|
'sourceType' => $sourceMode,
|
||||||
|
'sourceListingId' => $sourceMode === 'custom' ? null : $sourceListingId,
|
||||||
'street' => $street ?: null,
|
'street' => $street ?: null,
|
||||||
'zip' => $zip,
|
'zip' => $zip,
|
||||||
'city' => $city,
|
'city' => $city,
|
||||||
@@ -712,8 +746,9 @@ final class AccountPages
|
|||||||
$listingCategories = $listingCatalog ? $listingCatalog->listCategories(['general', 'place', 'food', 'event', 'family']) : [];
|
$listingCategories = $listingCatalog ? $listingCatalog->listCategories(['general', 'place', 'food', 'event', 'family']) : [];
|
||||||
$categoryReviewItems = $listingCatalog && $canManageSystemSettings ? $listingCatalog->listCategoryReviewItems() : [];
|
$categoryReviewItems = $listingCatalog && $canManageSystemSettings ? $listingCatalog->listCategoryReviewItems() : [];
|
||||||
$placeSuggestions = $listingCatalog ? $listingCatalog->listPlaceSuggestions(60) : [];
|
$placeSuggestions = $listingCatalog ? $listingCatalog->listPlaceSuggestions(60) : [];
|
||||||
|
$eventLocationOptions = $listingCatalog ? $listingCatalog->listEventLocationOptions(120) : [];
|
||||||
$stmt = $pdo?->prepare(
|
$stmt = $pdo?->prepare(
|
||||||
'SELECT e.id, e.title, e.teaser_public, e.description, e.category_slug, e.image_path, e.location_label, e.street, e.zip, e.city, e.region, e.starts_at, e.max_participants, e.allow_kids, e.visibility, e.status, e.lat, e.lng,
|
'SELECT e.id, e.title, e.teaser_public, e.description, e.category_slug, e.image_path, e.location_label, e.location_source_type, e.location_source_listing_id, e.street, e.zip, e.city, e.region, e.starts_at, e.max_participants, e.allow_kids, e.visibility, e.status, e.lat, e.lng,
|
||||||
(SELECT COUNT(*) FROM event_participants ep WHERE ep.event_id = e.id) AS participant_count
|
(SELECT COUNT(*) FROM event_participants ep WHERE ep.event_id = e.id) AS participant_count
|
||||||
FROM events e
|
FROM events e
|
||||||
WHERE e.created_by = :id AND e.starts_at >= NOW()
|
WHERE e.created_by = :id AND e.starts_at >= NOW()
|
||||||
@@ -832,6 +867,7 @@ final class AccountPages
|
|||||||
'listingCategories',
|
'listingCategories',
|
||||||
'categoryReviewItems',
|
'categoryReviewItems',
|
||||||
'placeSuggestions',
|
'placeSuggestions',
|
||||||
|
'eventLocationOptions',
|
||||||
'communityPoints',
|
'communityPoints',
|
||||||
'communityLevel',
|
'communityLevel',
|
||||||
'communityRoles',
|
'communityRoles',
|
||||||
@@ -980,6 +1016,8 @@ final class AccountPages
|
|||||||
$columns = [
|
$columns = [
|
||||||
'category_slug' => 'ALTER TABLE events ADD COLUMN category_slug VARCHAR(120) NULL AFTER description',
|
'category_slug' => 'ALTER TABLE events ADD COLUMN category_slug VARCHAR(120) NULL AFTER description',
|
||||||
'image_path' => 'ALTER TABLE events ADD COLUMN image_path VARCHAR(255) NULL AFTER category_slug',
|
'image_path' => 'ALTER TABLE events ADD COLUMN image_path VARCHAR(255) NULL AFTER category_slug',
|
||||||
|
'location_source_type' => 'ALTER TABLE events ADD COLUMN location_source_type ENUM("custom","place","editorial_event") NOT NULL DEFAULT "custom" AFTER location_label',
|
||||||
|
'location_source_listing_id' => 'ALTER TABLE events ADD COLUMN location_source_listing_id BIGINT UNSIGNED NULL AFTER location_source_type',
|
||||||
];
|
];
|
||||||
foreach ($columns as $column => $sql) {
|
foreach ($columns as $column => $sql) {
|
||||||
$stmt = $pdo->prepare('SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = "events" AND column_name = :column LIMIT 1');
|
$stmt = $pdo->prepare('SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = "events" AND column_name = :column LIMIT 1');
|
||||||
|
|||||||
@@ -493,6 +493,81 @@ final class ListingCatalog
|
|||||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function listEventLocationOptions(int $limit = 80): array
|
||||||
|
{
|
||||||
|
$this->ensureSchema();
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT l.id,
|
||||||
|
l.listing_type,
|
||||||
|
l.title,
|
||||||
|
l.status,
|
||||||
|
lp.street,
|
||||||
|
lp.zip,
|
||||||
|
lp.city,
|
||||||
|
lp.region,
|
||||||
|
lp.lat,
|
||||||
|
lp.lng,
|
||||||
|
lp.place_kind,
|
||||||
|
lo.starts_at,
|
||||||
|
lo.recurrence_until,
|
||||||
|
lc.slug AS category_slug,
|
||||||
|
lc.title AS category_title
|
||||||
|
FROM listings l
|
||||||
|
LEFT JOIN listing_places lp ON lp.id = l.primary_place_id
|
||||||
|
LEFT JOIN listing_occurrences lo ON lo.listing_id = l.id
|
||||||
|
LEFT JOIN listing_category_map lcm ON lcm.listing_id = l.id
|
||||||
|
LEFT JOIN listing_categories lc ON lc.id = lcm.category_id
|
||||||
|
WHERE l.listing_type IN ("place","editorial_event")
|
||||||
|
AND l.status IN ("draft","published")
|
||||||
|
ORDER BY
|
||||||
|
CASE l.listing_type
|
||||||
|
WHEN "place" THEN 1
|
||||||
|
WHEN "editorial_event" THEN 2
|
||||||
|
ELSE 3
|
||||||
|
END,
|
||||||
|
l.title ASC,
|
||||||
|
COALESCE(lo.starts_at, l.created_at) ASC
|
||||||
|
LIMIT :limit'
|
||||||
|
);
|
||||||
|
$stmt->bindValue(':limit', max(1, min(250, $limit)), \PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEventLocationSource(int $listingId): ?array
|
||||||
|
{
|
||||||
|
$this->ensureSchema();
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT l.id,
|
||||||
|
l.listing_type,
|
||||||
|
l.title,
|
||||||
|
l.status,
|
||||||
|
lp.street,
|
||||||
|
lp.zip,
|
||||||
|
lp.city,
|
||||||
|
lp.region,
|
||||||
|
lp.lat,
|
||||||
|
lp.lng,
|
||||||
|
lp.place_kind,
|
||||||
|
lo.starts_at,
|
||||||
|
lo.recurrence_until,
|
||||||
|
lc.slug AS category_slug,
|
||||||
|
lc.title AS category_title
|
||||||
|
FROM listings l
|
||||||
|
LEFT JOIN listing_places lp ON lp.id = l.primary_place_id
|
||||||
|
LEFT JOIN listing_occurrences lo ON lo.listing_id = l.id
|
||||||
|
LEFT JOIN listing_category_map lcm ON lcm.listing_id = l.id
|
||||||
|
LEFT JOIN listing_categories lc ON lc.id = lcm.category_id
|
||||||
|
WHERE l.id = :id
|
||||||
|
AND l.listing_type IN ("place","editorial_event")
|
||||||
|
AND l.status IN ("draft","published")
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$stmt->execute(['id' => $listingId]);
|
||||||
|
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
return $row ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
public function listUserOpenModerationRequestMap(int $userId): array
|
public function listUserOpenModerationRequestMap(int $userId): array
|
||||||
{
|
{
|
||||||
$this->ensureSchema();
|
$this->ensureSchema();
|
||||||
|
|||||||
Reference in New Issue
Block a user