dsad
All checks were successful
Deploy / deploy (push) Successful in 55s

This commit is contained in:
2026-08-11 21:58:47 +02:00
parent 6ca7d6d240
commit 016cf17971
4 changed files with 92 additions and 42 deletions

View File

@@ -35,6 +35,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- 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.
- Die Auswahl für Datenbank-Locations läuft im Event-Formular jetzt über ein echtes Auswahlfeld statt über Freitext; `Ort aus Datenbank` zeigt nur Orte, `Veranstaltung aus Datenbank` nur aktuell laufende Veranstaltungen im gültigen Zeitraum.
- Im Formular für eigene Events sind `Gültig bis` und `Wiederholung` ausgeblendet.
- Bei eigener Event-Location aus der Datenbank werden Adresse und Kategorie nur als Info angezeigt und nicht direkt bearbeitet.
- Aus dem Formular für eigene Events kann direkt in das Anlegen eines neuen Orts oder einer neuen Veranstaltung gewechselt werden.

View File

@@ -35,6 +35,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local 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.
- Database-backed event locations now use a real select field instead of free text; `place from database` only shows places, while `event from database` only shows currently active event-style entries inside their valid date window.
- Inside the own-event form, `valid until` and `recurrence` are hidden.
- When an own event uses a database-backed location, address and category are shown as informational values and are not edited directly.
- The own-event form now offers direct links into creating a new place or a new event-style listing.

View File

@@ -61,14 +61,20 @@ foreach (($listingCategories ?? []) as $categoryRow) {
$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 = '';
$usesDatabaseEventLocation = $currentEntryKind === 'event' && $eventLocationModeValue !== 'custom';
$eventAddressInfoValue = trim(implode(', ', array_filter([
$streetValue,
trim($zipValue . ' ' . $cityValue),
$regionValue,
])));
$eventLocationPlaceOptions = [];
$eventLocationEditorialOptions = [];
foreach (($eventLocationOptions ?? []) as $locationOption) {
if ((string)($locationOption['listing_type'] ?? '') === 'place') {
$eventLocationPlaceOptions[] = $locationOption;
} elseif ((string)($locationOption['listing_type'] ?? '') === 'editorial_event') {
$eventLocationEditorialOptions[] = $locationOption;
}
if ((int)($locationOption['id'] ?? 0) !== $eventLocationSourceIdValue) {
continue;
}
@@ -78,12 +84,6 @@ foreach (($eventLocationOptions ?? []) as $locationOption) {
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);
@@ -1144,25 +1144,6 @@ if (!empty($canManageSystemSettings)) {
</option>
<?php endforeach; ?>
</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): ?>
<div class="stack gap-8">
<label class="label">Was möchtest du anlegen?</label>
@@ -1197,8 +1178,29 @@ if (!empty($canManageSystemSettings)) {
</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) ?>">
<label class="label" for="eventLocationSourceSelect" id="eventLocationSourceLabel">Ort aus Datenbank</label>
<select id="eventLocationSourceSelect" class="select">
<option value=""><?= $eventLocationModeValue === 'editorial_event' ? 'Keine aktuell verfügbaren Veranstaltungen' : 'Ort aus der Datenbank auswählen' ?></option>
<?php foreach (($eventLocationModeValue === 'editorial_event' ? $eventLocationEditorialOptions : $eventLocationPlaceOptions) as $locationOption): ?>
<?php
$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([
(string)($locationOption['title'] ?? ''),
$optionDateLabel,
$optionAddressLabel,
])));
?>
<option
value="<?= (int)($locationOption['id'] ?? 0) ?>"
data-location='<?= htmlspecialchars(json_encode($locationOption, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES) ?>'
<?= (int)($locationOption['id'] ?? 0) === $eventLocationSourceIdValue ? 'selected' : '' ?>
><?= htmlspecialchars($optionLabel, ENT_QUOTES) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if (($section ?? 'events') === 'events' && !$editingListing): ?>
<div class="flex gap-8" style="flex-wrap:wrap;" id="eventCreateListingActionsWrap">
@@ -1450,7 +1452,7 @@ if (!empty($canManageSystemSettings)) {
const eventLocationModeWrap = document.getElementById('eventLocationModeWrap');
const eventLocationModeSelect = document.getElementById('eventLocationMode');
const eventLocationSourceWrap = document.getElementById('eventLocationSourceWrap');
const eventLocationSourceInput = document.getElementById('eventLocationSourceInput');
const eventLocationSourceSelect = document.getElementById('eventLocationSourceSelect');
const eventLocationSourceIdInput = document.getElementById('eventLocationSourceId');
const eventLocationSourceLabel = document.getElementById('eventLocationSourceLabel');
const categoryInputWrap = document.getElementById('categoryInputWrap');
@@ -1532,16 +1534,50 @@ if (!empty($canManageSystemSettings)) {
});
}
function findEventLocationOptionByLabel(label) {
const normalized = (label || '').trim();
if (!normalized) return null;
const option = document.querySelector(`#eventLocationOptionList option[value="${CSS.escape(normalized)}"]`);
const allEventLocationOptions = <?= json_encode($eventLocationOptions ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
function buildEventLocationLabel(option) {
if (!option) return '';
const addressLabel = [String(option.street || ''), `${String(option.zip || '')} ${String(option.city || '')}`.trim()]
.filter(Boolean)
.join(', ');
const dateLabel = option.starts_at ? new Date(String(option.starts_at)).toLocaleDateString('de-DE') : '';
return [String(option.title || ''), dateLabel, addressLabel].filter(Boolean).join(' · ');
}
function findEventLocationOptionById(id) {
const normalizedId = Number(id || 0);
if (!normalizedId) return null;
const option = allEventLocationOptions.find((entry) => Number(entry.id || 0) === normalizedId);
if (!option) return null;
try {
return JSON.parse(option.getAttribute('data-location') || 'null');
} catch (error) {
return null;
}
return option;
}
function rebuildEventLocationOptions() {
if (!eventLocationSourceSelect) return;
const mode = eventLocationModeSelect?.value || 'custom';
const selectedId = Number(eventLocationSourceIdInput?.value || eventLocationSourceSelect.value || 0);
const filteredOptions = allEventLocationOptions.filter((option) => String(option.listing_type || '') === mode);
const emptyLabel = mode === 'editorial_event'
? 'Keine aktuell verfügbaren Veranstaltungen'
: 'Ort aus der Datenbank auswählen';
eventLocationSourceSelect.innerHTML = '';
const placeholderOption = document.createElement('option');
placeholderOption.value = '';
placeholderOption.textContent = filteredOptions.length === 0 ? emptyLabel : (mode === 'editorial_event' ? 'Veranstaltung aus der Datenbank auswählen' : 'Ort aus der Datenbank auswählen');
eventLocationSourceSelect.appendChild(placeholderOption);
eventLocationSourceSelect.disabled = filteredOptions.length === 0;
filteredOptions.forEach((option) => {
const element = document.createElement('option');
element.value = String(option.id || '');
element.textContent = buildEventLocationLabel(option);
if (Number(option.id || 0) === selectedId) {
element.selected = true;
}
eventLocationSourceSelect.appendChild(element);
});
}
function applyEventLocationOption(option) {
@@ -1585,7 +1621,7 @@ if (!empty($canManageSystemSettings)) {
function clearEventLocationSelection() {
if (eventLocationSourceIdInput) eventLocationSourceIdInput.value = '';
if (eventLocationSourceInput) eventLocationSourceInput.value = '';
if (eventLocationSourceSelect) eventLocationSourceSelect.value = '';
if (streetInput) streetInput.value = '';
if (zipInput) zipInput.value = '';
if (cityInput) cityInput.value = '';
@@ -1614,6 +1650,7 @@ if (!empty($canManageSystemSettings)) {
if (eventLocationSourceLabel) {
eventLocationSourceLabel.textContent = mode === 'editorial_event' ? 'Veranstaltung aus Datenbank' : 'Ort aus Datenbank';
}
rebuildEventLocationOptions();
if (categoryInput) {
if (usesDatabaseLocation) {
@@ -1624,7 +1661,7 @@ if (!empty($canManageSystemSettings)) {
}
if (usesDatabaseLocation) {
const option = findEventLocationOptionByLabel(eventLocationSourceInput?.value || '');
const option = findEventLocationOptionById(eventLocationSourceSelect?.value || eventLocationSourceIdInput?.value || '');
if (option && String(option.listing_type || '') === mode) {
applyEventLocationOption(option);
} else {
@@ -1683,9 +1720,9 @@ if (!empty($canManageSystemSettings)) {
clearEventLocationSelection();
syncEventLocationMode();
});
eventLocationSourceInput?.addEventListener('change', () => {
eventLocationSourceSelect?.addEventListener('change', () => {
const mode = eventLocationModeSelect?.value || 'custom';
const option = findEventLocationOptionByLabel(eventLocationSourceInput.value || '');
const option = findEventLocationOptionById(eventLocationSourceSelect.value || '');
if (!option || String(option.listing_type || '') !== mode) {
clearEventLocationSelection();
return;

View File

@@ -519,6 +519,17 @@ final class ListingCatalog
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")
AND (
l.listing_type = "place"
OR (
lo.starts_at IS NOT NULL
AND DATE(lo.starts_at) <= CURRENT_DATE()
AND (
lo.recurrence_until IS NULL
OR DATE(lo.recurrence_until) >= CURRENT_DATE()
)
)
)
ORDER BY
CASE l.listing_type
WHEN "place" THEN 1