1882 lines
103 KiB
PHP
Executable File
1882 lines
103 KiB
PHP
Executable File
<?php
|
|
$app = app();
|
|
$vm = \App\AccountPages::dashboard($app);
|
|
extract($vm, EXTR_OVERWRITE);
|
|
$avatarPlaceholder = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 12 12%22%3E%3Crect width=%2212%22 height=%2212%22 fill=%22%23f7efe6%22/%3E%3C/svg%3E';
|
|
$avatarStyleKeys = array_keys($avatarBuilder ?? []);
|
|
$currentAvatarStyle = (string)($profile['avatar_style'] ?? ($avatarStyleKeys[0] ?? 'lorelei'));
|
|
$dashboardUserId = (int)($_SESSION['user_id'] ?? 0);
|
|
$editingEvent = isset($editEvent) && $editEvent !== null;
|
|
$editingListing = isset($editListing) && $editListing !== null;
|
|
$editing = $editingEvent || $editingListing;
|
|
$currentEntryKind = $editingListing ? (string)($editListing['listing_type'] ?? 'editorial_event') : 'event';
|
|
$actionEvent = $editing ? 'event_update' : 'event_add';
|
|
$startVal = '';
|
|
if ($editingEvent && !empty($editEvent['starts_at'])) {
|
|
$startVal = date('Y-m-d', strtotime((string)$editEvent['starts_at']));
|
|
} elseif ($editingListing && !empty($editListing['starts_at'])) {
|
|
$startVal = date('Y-m-d', strtotime((string)$editListing['starts_at']));
|
|
}
|
|
$withChildrenValue = $editingEvent ? (((int)($editEvent['allow_kids'] ?? 1)) === 1 ? 'yes' : 'no') : (($editingListing && $currentEntryKind === 'editorial_event' && !empty($editListing['supports_registration'])) ? 'yes' : 'yes');
|
|
$eventTitle = $editingEvent ? 'Event bearbeiten' : ($editingListing ? ($currentEntryKind === 'place' ? 'Ort bearbeiten' : 'Veranstaltung bearbeiten') : 'Neuen Eintrag anlegen');
|
|
$submitLabel = $editingEvent ? 'Event speichern' : ($editingListing ? ($currentEntryKind === 'place' ? 'Ort speichern' : 'Veranstaltung speichern') : 'Eintrag anlegen');
|
|
$titleValue = $editingEvent ? (string)($editEvent['title'] ?? '') : (string)($editListing['title'] ?? '');
|
|
$teaserValue = $editingEvent ? (string)($editEvent['teaser_public'] ?? '') : (string)($editListing['teaser_public'] ?? '');
|
|
$descriptionValue = $editingEvent ? (string)($editEvent['description'] ?? '') : (string)($editListing['description'] ?? '');
|
|
$streetValue = $editingEvent ? (string)($editEvent['street'] ?? '') : (string)($editListing['street'] ?? '');
|
|
$zipValue = $editingEvent ? (string)($editEvent['zip'] ?? '') : (string)($editListing['zip'] ?? '');
|
|
$cityValue = $editingEvent ? (string)($editEvent['city'] ?? '') : (string)($editListing['city'] ?? '');
|
|
$regionValue = $editingEvent ? (string)($editEvent['region'] ?? '') : (string)($editListing['region'] ?? '');
|
|
$latValue = $editingEvent ? (string)($editEvent['lat'] ?? '') : (string)($editListing['lat'] ?? '');
|
|
$lngValue = $editingEvent ? (string)($editEvent['lng'] ?? '') : (string)($editListing['lng'] ?? '');
|
|
$visibilityValue = $editingEvent ? (string)($editEvent['visibility'] ?? 'public') : (string)($editListing['visibility'] ?? 'public');
|
|
$categorySlugValue = $editingEvent ? (string)($editEvent['category_slug'] ?? '') : (string)($editListing['category_slug'] ?? '');
|
|
$imagePathValue = $editingEvent ? (string)($editEvent['image_path'] ?? '') : (string)($editListing['image_path'] ?? '');
|
|
$capacityValue = $editingEvent ? (string)($editEvent['max_participants'] ?? '') : (string)($editListing['capacity_total'] ?? '');
|
|
$phoneValue = $editingListing ? (string)($editListing['phone'] ?? '') : '';
|
|
$websiteValue = $editingListing ? (string)($editListing['website_url'] ?? '') : '';
|
|
$specialConditionsValue = $editingListing ? (string)($editListing['special_conditions_note'] ?? '') : '';
|
|
$openingHoursValue = $editingListing ? (string)($editListing['opening_hours_note'] ?? '') : '';
|
|
$openingHoursRows = $editingListing && !empty($editListing['opening_hours']) && is_array($editListing['opening_hours']) ? $editListing['opening_hours'] : [['day_label' => '', 'time_from' => '', 'time_to' => '', 'note' => '']];
|
|
$recurrenceRuleValue = $editingListing ? (string)($editListing['recurrence_rule'] ?? '') : '';
|
|
$recurrenceUntilValue = '';
|
|
if ($editingListing && !empty($editListing['recurrence_until'])) {
|
|
$recurrenceUntilValue = date('Y-m-d', strtotime((string)$editListing['recurrence_until']));
|
|
}
|
|
$recurrenceModeValue = 'single';
|
|
if (str_contains($recurrenceRuleValue, 'FREQ=DAILY')) {
|
|
$recurrenceModeValue = 'daily';
|
|
} elseif (str_contains($recurrenceRuleValue, 'FREQ=WEEKLY')) {
|
|
$recurrenceModeValue = 'weekly';
|
|
}
|
|
$categoryTitleMap = [];
|
|
foreach (($listingCategories ?? []) as $categoryRow) {
|
|
$categoryTitleMap[(string)($categoryRow['slug'] ?? '')] = (string)($categoryRow['title'] ?? '');
|
|
}
|
|
$categoryInputValue = $categoryTitleMap[$categorySlugValue] ?? $categorySlugValue;
|
|
$priceRows = [];
|
|
if ($editingListing && !empty($editListing['prices']) && is_array($editListing['prices'])) {
|
|
foreach ($editListing['prices'] as $priceRow) {
|
|
$ageFrom = '';
|
|
$ageTo = '';
|
|
$noteValue = (string)($priceRow['note'] ?? '');
|
|
if (preg_match('/ab\s+(\d+)/u', $noteValue, $match)) {
|
|
$ageFrom = $match[1];
|
|
}
|
|
if (preg_match('/bis\s+(\d+)/u', $noteValue, $match)) {
|
|
$ageTo = $match[1];
|
|
}
|
|
$priceRows[] = [
|
|
'amount' => (string)($priceRow['amount'] ?? ''),
|
|
'age_from' => $ageFrom,
|
|
'age_to' => $ageTo,
|
|
];
|
|
}
|
|
}
|
|
if ($priceRows === []) {
|
|
$priceRows = [[
|
|
'amount' => '',
|
|
'age_from' => '',
|
|
'age_to' => '',
|
|
]];
|
|
}
|
|
$editingChild = isset($editChild) && $editChild !== null;
|
|
$childAction = $editingChild ? 'child_update' : 'child_add';
|
|
$childModalTitle = $editingChild ? 'Kind bearbeiten' : 'Kind hinzufügen';
|
|
$sectionLinks = [
|
|
'profile' => 'Profil',
|
|
'children' => 'Kinder',
|
|
'events' => 'Events',
|
|
'community' => 'Community',
|
|
'settings' => 'Einstellungen',
|
|
];
|
|
if (!empty($canManageSystemSettings)) {
|
|
$sectionLinks['system'] = 'System';
|
|
}
|
|
?>
|
|
<main class="section">
|
|
<div class="container" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;">
|
|
<div>
|
|
<p class="eyebrow">Mitgliederbereich</p>
|
|
<h1>Hallo, <span style="color: var(--color-primary);"><?= htmlspecialchars($profile['display_name'] ?: 'Papa', ENT_QUOTES) ?></span>!</h1>
|
|
<p class="muted">Verwalte dein Profil, Kinder, Events und Teilnahmen.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container dash-section">
|
|
<?php if ($flash): ?>
|
|
<div class="toast-bar"><?= htmlspecialchars($flash['message'], ENT_QUOTES) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($info): ?>
|
|
<div class="toast-bar" style="margin-top:10px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($error): ?>
|
|
<div class="toast-bar" style="margin-top:10px; border-color:#f87171; color:#991b1b;">Fehler: <?= htmlspecialchars($error, ENT_QUOTES) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="container dash-section">
|
|
<div class="account-layout">
|
|
<aside class="account-sidebar">
|
|
<div class="account-sidebar__inner">
|
|
<div class="account-sidebar__head">
|
|
<span class="badge">Mitgliederbereich</span>
|
|
<h2>Navigation</h2>
|
|
</div>
|
|
<nav class="account-sidebar__nav">
|
|
<?php foreach ($sectionLinks as $sectionKey => $sectionLabel): ?>
|
|
<a href="/dashboard?section=<?= rawurlencode($sectionKey) ?>" class="<?= $section === $sectionKey ? 'is-active' : '' ?>">
|
|
<span><?= htmlspecialchars($sectionLabel, ENT_QUOTES) ?></span>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</nav>
|
|
</div>
|
|
</aside>
|
|
|
|
<div class="account-main">
|
|
<?php if ($section === 'profile'): ?>
|
|
<section class="account-panel">
|
|
<div class="account-panel__head">
|
|
<h2>Dein Profil</h2>
|
|
<p class="muted">Hier pflegst du deine persönlichen Angaben für den Mitgliederbereich.</p>
|
|
</div>
|
|
<div class="account-panel__body">
|
|
<div class="profile-avatar-panel">
|
|
<div class="profile-avatar-panel__preview">
|
|
<?= \App\Avatar\AvatarManager::render($profile, $profile['display_name'] ?: 'Papa', 'xl', $dashboardUserId) ?>
|
|
</div>
|
|
<div class="profile-avatar-panel__content">
|
|
<h3>Dein Profilbild</h3>
|
|
<p class="muted">Dein Profilbild wird mit dem Avatar-Generator erzeugt und als feste SVG-Datei zu deinem Konto gespeichert. Stil, Seed und Varianten bleiben dabei in deinem Profil hinterlegt.</p>
|
|
<button class="btn" type="button" data-modal-open="modalAvatar">Profilbild gestalten</button>
|
|
</div>
|
|
</div>
|
|
<div class="profile-summary-grid">
|
|
<section class="profile-summary-card">
|
|
<span class="profile-summary-card__eyebrow">Basis</span>
|
|
<dl class="account-kv account-kv--compact">
|
|
<div><dt>Anzeigename</dt><dd><?= htmlspecialchars($profile['display_name'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
|
|
<div><dt>Name</dt><dd><?= htmlspecialchars(trim($profile['first_name'] . ' ' . $profile['last_name']) ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
|
|
<div><dt>E-Mail</dt><dd><?= htmlspecialchars($profile['email'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
|
|
<div><dt>Telefon</dt><dd><?= htmlspecialchars($profile['contact_phone'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
|
|
</dl>
|
|
</section>
|
|
<section class="profile-summary-card">
|
|
<span class="profile-summary-card__eyebrow">Standort</span>
|
|
<dl class="account-kv account-kv--compact">
|
|
<div><dt>Adresse</dt><dd><?= htmlspecialchars(trim(implode(', ', array_filter([(string)($profile['street'] ?? ''), trim((string)$profile['zip'] . ' ' . (string)$profile['city']), (string)($profile['region'] ?? '')]))) ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
|
|
<div><dt>Sprachen</dt><dd><?= htmlspecialchars($profile['languages'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
|
|
<div><dt>Beruf</dt><dd><?= htmlspecialchars($profile['profession'] ?: 'Nicht hinterlegt', ENT_QUOTES) ?></dd></div>
|
|
</dl>
|
|
</section>
|
|
</div>
|
|
<?php if (trim((string)$profile['about']) !== ''): ?>
|
|
<div class="profile-about-card">
|
|
<strong>Über dich</strong>
|
|
<p><?= nl2br(htmlspecialchars((string)$profile['about'], ENT_QUOTES)) ?></p>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="flex gap-12" style="margin-top:18px;">
|
|
<button class="btn" type="button" data-modal-open="modalProfile">Profil bearbeiten</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($section === 'children'): ?>
|
|
<section class="account-panel">
|
|
<div class="account-panel__head">
|
|
<h2>Deine Kinder</h2>
|
|
<p class="muted">Optionale Angaben, die dir bei Planung und passenden Events und Treffen helfen können.</p>
|
|
</div>
|
|
<div class="account-panel__body">
|
|
<?php if (!$children): ?>
|
|
<p class="muted small">Noch keine Kinder eingetragen.</p>
|
|
<?php else: ?>
|
|
<ul class="dash-list">
|
|
<?php foreach ($children as $c): ?>
|
|
<li>
|
|
<div style="display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap:wrap;">
|
|
<div>
|
|
<strong><?= htmlspecialchars($c['first_name'], ENT_QUOTES) ?></strong>,
|
|
<?= htmlspecialchars($c['gender'], ENT_QUOTES) ?>
|
|
<?= $c['age_years'] ? '(' . (int)$c['age_years'] . ' Jahre)' : '' ?>
|
|
<?php if (!empty($c['birthdate'])): ?>
|
|
<span class="muted small">· geboren am <?= htmlspecialchars(date('d.m.Y', strtotime((string)$c['birthdate'])), ENT_QUOTES) ?></span>
|
|
<?php endif; ?>
|
|
<?php if (!empty($c['note'])): ?>
|
|
<div class="muted small" style="margin-top:4px;"><?= htmlspecialchars($c['note'], ENT_QUOTES) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="flex gap-12" style="flex-wrap:wrap;">
|
|
<a class="btn ghost" href="/dashboard?section=children&edit_child=<?= (int)$c['id'] ?>">Bearbeiten</a>
|
|
<form method="post" action="/dashboard?section=children" onsubmit="return confirm('Kind wirklich löschen?');">
|
|
<input type="hidden" name="action" value="child_delete">
|
|
<input type="hidden" name="child_id" value="<?= (int)$c['id'] ?>">
|
|
<button class="btn ghost" type="submit">Löschen</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php endif; ?>
|
|
<div class="flex gap-12" style="margin-top:18px;">
|
|
<button class="btn" type="button" data-modal-open="modalChild">Kind hinzufügen</button>
|
|
<?php if ($editingChild): ?>
|
|
<a class="btn ghost" href="/dashboard?section=children">Bearbeitung abbrechen</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($section === 'community'): ?>
|
|
<?php
|
|
$showCommunityRestrictionState = !empty($communityRestrictions['thread_create_blocked'])
|
|
|| !empty($communityRestrictions['reply_blocked']);
|
|
?>
|
|
<section class="account-panel">
|
|
<div class="account-panel__head">
|
|
<h2>Dein Community-Status</h2>
|
|
<p class="muted">Rang, Punkte und nur bei Bedarf ein Hinweis zu aktiven Schreibsperren.</p>
|
|
</div>
|
|
<div class="account-panel__body">
|
|
<dl class="account-kv">
|
|
<div><dt>Rang</dt><dd><?= htmlspecialchars(trim(($communityLevel['icon'] ?? '') . ' ' . ($communityLevel['label'] ?? '')), ENT_QUOTES) ?></dd></div>
|
|
<div><dt>Punkte</dt><dd><?= number_format((float)$communityPoints, 1, ',', '.') ?></dd></div>
|
|
<?php if ($showCommunityRestrictionState): ?>
|
|
<div><dt>Themen erstellen</dt><dd><?= $communityRestrictions['thread_create_blocked'] ? 'Gesperrt' : 'Erlaubt' ?></dd></div>
|
|
<div><dt>Antworten schreiben</dt><dd><?= $communityRestrictions['reply_blocked'] ? 'Gesperrt' : 'Erlaubt' ?></dd></div>
|
|
<?php endif; ?>
|
|
</dl>
|
|
<?php if (!empty($communityRestrictions['reason'])): ?>
|
|
<p class="muted small" style="margin-top:14px;">Hinweis zur Community-Sperre: <?= htmlspecialchars((string)$communityRestrictions['reason'], ENT_QUOTES) ?></p>
|
|
<?php endif; ?>
|
|
<?php if ($communityCanApply): ?>
|
|
<form method="post" class="stack gap-6" style="margin-top:18px;">
|
|
<input type="hidden" name="action" value="community_admin_apply">
|
|
<label class="label" for="communityMotivation">Bewerbung als Forum-Admin</label>
|
|
<textarea id="communityMotivation" name="motivation" class="textarea" rows="4" placeholder="Warum möchtest du die Community als Forum-Admin unterstützen?" required></textarea>
|
|
<button class="btn" type="submit">Bewerbung absenden</button>
|
|
</form>
|
|
<?php elseif ($communityApplication): ?>
|
|
<div style="margin-top:18px;">
|
|
<p class="muted small" style="margin:0;">Letzte Bewerbung: <strong><?= htmlspecialchars((string)$communityApplication['status'], ENT_QUOTES) ?></strong></p>
|
|
<?php if (!empty($communityApplication['decision_reason'])): ?>
|
|
<p class="muted small" style="margin-top:6px;"><?= htmlspecialchars((string)$communityApplication['decision_reason'], ENT_QUOTES) ?></p>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($section === 'events'): ?>
|
|
<section class="account-panel" id="events">
|
|
<div class="account-panel__head">
|
|
<h2>Deine Einträge</h2>
|
|
<p class="muted">Lege eigene Events, dauerhafte Orte und sonstige zeitlich begrenzte Veranstaltungen sauber getrennt an.</p>
|
|
</div>
|
|
<div class="account-panel__body">
|
|
<div class="flex gap-12" style="margin:0 0 16px 0; flex-wrap: wrap;">
|
|
<button class="btn" type="button" data-modal-open="modalEvent">Eintrag anlegen</button>
|
|
</div>
|
|
<div class="card" style="margin-bottom:18px;">
|
|
<strong>Eigene Events</strong>
|
|
<p class="muted small" style="margin:8px 0 0;">Von dir organisierte Treffen oder Events mit eigener Teilnahme-Logik.</p>
|
|
</div>
|
|
<?php if (!$eventsUpcoming): ?>
|
|
<p class="muted small">Keine zukünftigen eigenen Events angelegt.</p>
|
|
<?php else: ?>
|
|
<ul class="dash-list" style="margin-top:10px;">
|
|
<?php foreach ($eventsUpcoming as $e): ?>
|
|
<li>
|
|
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap: wrap;">
|
|
<div>
|
|
<strong><?= htmlspecialchars($e['title'], ENT_QUOTES) ?></strong>
|
|
<?php if ($e['status'] === 'cancelled'): ?>
|
|
<span class="badge" style="background:#fee2e2; color:#991b1b;">Abgesagt</span>
|
|
<?php endif; ?>
|
|
<div class="muted small" style="margin-top:4px;">
|
|
<?= htmlspecialchars($e['city'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['starts_at'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['visibility'], ENT_QUOTES) ?>
|
|
· Anmeldungen: <?= (int)$e['participant_count'] ?>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-8" style="flex-wrap: wrap;">
|
|
<a class="btn ghost" href="/dashboard?section=events&edit_event=<?= (int)$e['id'] ?>#events">Bearbeiten</a>
|
|
<?php if ((int)$e['participant_count'] === 0): ?>
|
|
<form method="post" action="/dashboard?section=events#events" onsubmit="return confirm('Event wirklich löschen?');">
|
|
<input type="hidden" name="action" value="event_delete">
|
|
<input type="hidden" name="event_id" value="<?= (int)$e['id'] ?>">
|
|
<button class="btn ghost" type="submit">Löschen</button>
|
|
</form>
|
|
<?php else: ?>
|
|
<?php if ($e['status'] !== 'cancelled'): ?>
|
|
<form method="post" action="/dashboard?section=events#events" onsubmit="return confirm('Event für alle absagen?');">
|
|
<input type="hidden" name="action" value="event_cancel">
|
|
<input type="hidden" name="event_id" value="<?= (int)$e['id'] ?>">
|
|
<button class="btn ghost" type="submit">Absagen</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php endif; ?>
|
|
<details style="margin-top:12px;">
|
|
<summary style="cursor:pointer;">Vergangene Events anzeigen</summary>
|
|
<?php if (!$eventsPast): ?>
|
|
<p class="muted small">Keine vergangenen Events.</p>
|
|
<?php else: ?>
|
|
<ul class="dash-list" style="margin-top:10px;">
|
|
<?php foreach ($eventsPast as $e): ?>
|
|
<li>
|
|
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap: wrap;">
|
|
<div>
|
|
<strong><?= htmlspecialchars($e['title'], ENT_QUOTES) ?></strong>
|
|
<?php if ($e['status'] === 'cancelled'): ?>
|
|
<span class="badge" style="background:#fee2e2; color:#991b1b;">Abgesagt</span>
|
|
<?php endif; ?>
|
|
<div class="muted small" style="margin-top:4px;">
|
|
<?= htmlspecialchars($e['city'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['starts_at'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['visibility'], ENT_QUOTES) ?>
|
|
· Anmeldungen: <?= (int)$e['participant_count'] ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php endif; ?>
|
|
</details>
|
|
|
|
<div class="card" style="margin:22px 0 14px;">
|
|
<strong>Orte und sonstige Veranstaltungen</strong>
|
|
<p class="muted small" style="margin:8px 0 0;">Hier erscheinen dauerhafte Orte wie Spielplätze, Cafés oder Restaurants sowie zeitlich begrenzte Hinweise wie Zirkus oder Aktionsflächen.</p>
|
|
</div>
|
|
<?php if (!$otherListings): ?>
|
|
<p class="muted small">Noch keine Orte oder sonstigen Veranstaltungen angelegt.</p>
|
|
<?php else: ?>
|
|
<ul class="dash-list" style="margin-top:10px;">
|
|
<?php foreach ($otherListings as $entry): ?>
|
|
<li>
|
|
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap: wrap;">
|
|
<div>
|
|
<strong><?= htmlspecialchars((string)$entry['title'], ENT_QUOTES) ?></strong>
|
|
<span class="badge"><?= ($entry['listing_type'] ?? '') === 'place' ? 'Ort' : 'Sonstige Veranstaltung' ?></span>
|
|
<?php if (!empty($entry['category_title'])): ?>
|
|
<span class="badge"><?= htmlspecialchars((string)$entry['category_title'], ENT_QUOTES) ?></span>
|
|
<?php endif; ?>
|
|
<div class="muted small" style="margin-top:4px;">
|
|
<?= htmlspecialchars(trim(implode(' · ', array_filter([
|
|
(string)($entry['city'] ?? ''),
|
|
!empty($entry['starts_at']) ? (string)$entry['starts_at'] : null,
|
|
(string)($entry['visibility'] ?? ''),
|
|
]))), ENT_QUOTES) ?>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-8" style="flex-wrap: wrap;">
|
|
<a class="btn ghost" href="/dashboard?section=events&edit_listing=<?= (int)$entry['id'] ?>#events">Bearbeiten</a>
|
|
<form method="post" action="/dashboard?section=events#events" onsubmit="return confirm('Eintrag wirklich löschen?');">
|
|
<input type="hidden" name="action" value="listing_delete">
|
|
<input type="hidden" name="listing_id" value="<?= (int)$entry['id'] ?>">
|
|
<button class="btn ghost" type="submit">Löschen</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php endif; ?>
|
|
</div>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($section === 'settings'): ?>
|
|
<section class="account-panel">
|
|
<div class="account-panel__head">
|
|
<h2>Einstellungen</h2>
|
|
<p class="muted">Standort, Browserfreigabe und Cookie-Einstellungen an einem Ort.</p>
|
|
</div>
|
|
<div class="account-panel__body">
|
|
<form method="post" class="stack gap-12">
|
|
<input type="hidden" name="action" value="settings_location">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pLocationPref">Standort auf Papa-Kind-Treff verwenden</label>
|
|
<select id="pLocationPref" name="location_tracking_preference" class="select">
|
|
<option value="disabled" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'disabled') ? 'selected' : '' ?>>Nie verwenden</option>
|
|
<option value="prompt" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'prompt') ? 'selected' : '' ?>>Bei Bedarf nachfragen</option>
|
|
<option value="enabled" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'enabled') ? 'selected' : '' ?>>Wenn im Browser erlaubt, automatisch verwenden</option>
|
|
</select>
|
|
<p class="muted small">Diese Einstellung steuert nur, ob Papa-Kind-Treff Standortdaten nutzen darf. Die eigentliche Browserfreigabe wird separat im Browser verwaltet.</p>
|
|
</div>
|
|
<div class="flex gap-12">
|
|
<button class="btn" type="submit">Einstellungen speichern</button>
|
|
<button class="btn ghost" type="button" data-consent-open>Cookie-Einstellungen öffnen</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="card" style="margin-top:18px;" id="locationBrowserStatusCard">
|
|
<strong>Browser-Status für Standort</strong>
|
|
<p class="muted small" id="locationBrowserStatusText" style="margin:8px 0 0;">Wird geprüft …</p>
|
|
<p class="muted small" id="locationBrowserStatusHint" style="margin:8px 0 0;">
|
|
Deine Website-Einstellung und die Browserfreigabe sind getrennte Ebenen. Wenn der Browser blockiert, kann Papa-Kind-Treff den Standort auch bei aktiver Einstellung nicht nutzen.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="settings-help" style="margin-top:18px;">
|
|
<details>
|
|
<summary>Warum gibt es zwei Einstellungen?</summary>
|
|
<p class="muted small" style="margin:10px 0 0;">Der Browser entscheidet technisch, ob Standortzugriffe erlaubt sind. Papa-Kind-Treff entscheidet mit deiner Website-Einstellung, ob diese Möglichkeit auf der Plattform überhaupt verwendet werden soll.</p>
|
|
</details>
|
|
<details>
|
|
<summary>Chrome und Edge</summary>
|
|
<p class="muted small" style="margin:10px 0 0;">Auf das Schloss-Symbol links neben der Adresszeile klicken, dann `Website-Einstellungen` oder `Standort` öffnen und die Freigabe dort anpassen.</p>
|
|
</details>
|
|
<details>
|
|
<summary>Firefox</summary>
|
|
<p class="muted small" style="margin:10px 0 0;">Auf das Schloss-Symbol links neben der Adresse klicken und unter Berechtigungen den Standort prüfen oder zurücksetzen. Alternativ über Einstellungen → Datenschutz & Sicherheit → Berechtigungen.</p>
|
|
</details>
|
|
<details>
|
|
<summary>Safari</summary>
|
|
<p class="muted small" style="margin:10px 0 0;">In Safari die Website-Einstellungen für den aktuellen Tab oder die Datenschutz-/Standort-Einstellungen des Browsers beziehungsweise des Geräts prüfen.</p>
|
|
</details>
|
|
<details>
|
|
<summary>Direkt in die Browser-Einstellungen springen?</summary>
|
|
<p class="muted small" style="margin:10px 0 0;">Nein, das kann eine Website nicht zuverlässig und browserübergreifend auslösen. Deshalb zeigt Papa-Kind-Treff nur den Status an und erklärt, wo du die Freigabe findest.</p>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($section === 'system' && !empty($canManageSystemSettings)): ?>
|
|
<section class="account-panel">
|
|
<div class="account-panel__head">
|
|
<h2>System-Einstellungen</h2>
|
|
<p class="muted">Globale Schalter für Wartung, externe Ortsdaten und die neue Termin-/Ort-Basis.</p>
|
|
</div>
|
|
<div class="account-panel__body">
|
|
<form method="post" class="stack gap-12">
|
|
<input type="hidden" name="action" value="system_settings_update">
|
|
|
|
<div class="card">
|
|
<strong>Wartung und Verfügbarkeit</strong>
|
|
<div class="stack gap-10" style="margin-top:12px;">
|
|
<label class="chip" style="cursor:pointer; align-items:flex-start;">
|
|
<input type="checkbox" name="forum_maintenance_mode" value="1" <?= !empty($systemSettingsValues['forum_maintenance_mode']) && $systemSettingsValues['forum_maintenance_mode'] === '1' ? 'checked' : '' ?> style="margin-right:8px; margin-top:4px;">
|
|
<span>Forum-Wartungsmodus aktivieren
|
|
<span class="muted small" style="display:block;">Die Community bleibt sichtbar, aber neue Themen und Antworten werden für normale Nutzer gesperrt.</span>
|
|
</span>
|
|
</label>
|
|
<label class="chip" style="cursor:pointer; align-items:flex-start;">
|
|
<input type="checkbox" name="site_maintenance_mode" value="1" <?= !empty($systemSettingsValues['site_maintenance_mode']) && $systemSettingsValues['site_maintenance_mode'] === '1' ? 'checked' : '' ?> style="margin-right:8px; margin-top:4px;">
|
|
<span>Seiten-Wartungsmodus aktivieren
|
|
<span class="muted small" style="display:block;">Normale Nutzer sehen dann statt der Website eine Wartungsseite. Seiten-Admins bleiben weiterhin im System.</span>
|
|
</span>
|
|
</label>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="siteMaintenanceMessage">Wartungshinweis</label>
|
|
<textarea id="siteMaintenanceMessage" name="site_maintenance_message" class="textarea" rows="3"><?= htmlspecialchars((string)($systemSettingsValues['site_maintenance_message'] ?? ''), ENT_QUOTES) ?></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<strong>Orts- und Bewertungsdienste</strong>
|
|
<div class="stack gap-10" style="margin-top:12px;">
|
|
<label class="chip" style="cursor:pointer; align-items:flex-start;">
|
|
<input type="checkbox" name="google_places_enabled" value="1" <?= !empty($systemSettingsValues['google_places_enabled']) && $systemSettingsValues['google_places_enabled'] === '1' ? 'checked' : '' ?> style="margin-right:8px; margin-top:4px;">
|
|
<span>Google Places vorbereitend aktivieren
|
|
<span class="muted small" style="display:block;">Aktuell noch ohne produktive Anbindung. Der Schalter ist als technische Vorbereitung für spätere Place-ID-, Bewertungs- und Review-Anreicherung gedacht.</span>
|
|
</span>
|
|
</label>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="placeDataProvider">Standardanbieter für Ortsdaten</label>
|
|
<select id="placeDataProvider" name="place_data_provider" class="select">
|
|
<option value="osm" <?= (($systemSettingsValues['place_data_provider'] ?? 'osm') === 'osm') ? 'selected' : '' ?>>OpenStreetMap / Nominatim</option>
|
|
<option value="osm_google_optional" <?= (($systemSettingsValues['place_data_provider'] ?? 'osm') === 'osm_google_optional') ? 'selected' : '' ?>>OpenStreetMap primär, Google optional</option>
|
|
</select>
|
|
<p class="muted small">So bleibt eine kostenfreie Basis aktiv, während Google später zusätzlich vorbereitet werden kann.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-12">
|
|
<button class="btn" type="submit">System-Einstellungen speichern</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="card" style="margin-top:18px;">
|
|
<strong>Status der neuen Termin-/Ort-Basis</strong>
|
|
<ul class="dash-list" style="margin-top:12px;">
|
|
<li>Neue Tabellenbasis: <?= !empty($listingCatalogStatus['complete']) ? 'Vollständig initialisiert' : 'Noch unvollständig' ?></li>
|
|
<li>Geplante Grundtypen: Event, Partner-Angebot, Ort, redaktioneller Termin</li>
|
|
<li>Geplante Zusatzlogik: Kategorien, Serientermine, Preise, Gutscheine, Kapazitäten</li>
|
|
<li>Fehlende Tabellen: <?= !empty($listingCatalogStatus['missing']) ? htmlspecialchars(implode(', ', $listingCatalogStatus['missing']), ENT_QUOTES) : 'Keine' ?></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:18px;">
|
|
<strong>Kategorien prüfen und zusammenführen</strong>
|
|
<p class="muted small" style="margin:10px 0 14px;">Neue Kategorien werden automatisch angelegt. Hier kannst du sie prüfen und bei Bedarf mit einer bestehenden Kategorie zusammenführen.</p>
|
|
<?php if (!empty($categoryReviewItems)): ?>
|
|
<div class="stack gap-10">
|
|
<?php foreach ($categoryReviewItems as $categoryItem): ?>
|
|
<div class="card" style="padding:12px;">
|
|
<div class="flex between center-y" style="gap:12px; flex-wrap:wrap;">
|
|
<div>
|
|
<strong><?= htmlspecialchars((string)$categoryItem['title'], ENT_QUOTES) ?></strong>
|
|
<?php if (!empty($categoryItem['needs_review'])): ?>
|
|
<span class="badge" style="margin-left:8px;">Neu</span>
|
|
<?php endif; ?>
|
|
<div class="muted small" style="margin-top:6px;">
|
|
Slug: <?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?> · Einträge: <?= (int)($categoryItem['listing_count'] ?? 0) ?> · Orte: <?= (int)($categoryItem['place_count'] ?? 0) ?> · Eigene Events: <?= (int)($categoryItem['event_count'] ?? 0) ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<form method="post" class="flex gap-8" style="margin-top:12px; flex-wrap:wrap;">
|
|
<input type="hidden" name="action" value="category_merge">
|
|
<input type="hidden" name="source_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
|
|
<select name="target_category_slug" class="select" style="min-width:260px;">
|
|
<option value="">Mit Kategorie zusammenführen</option>
|
|
<?php foreach ($listingCategories as $targetCategory): ?>
|
|
<?php if ((string)$targetCategory['slug'] === (string)$categoryItem['slug']) { continue; } ?>
|
|
<option value="<?= htmlspecialchars((string)$targetCategory['slug'], ENT_QUOTES) ?>"><?= htmlspecialchars((string)$targetCategory['title'], ENT_QUOTES) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<button class="btn ghost" type="submit">Zusammenführen</button>
|
|
</form>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php else: ?>
|
|
<p class="muted small" style="margin:12px 0 0;">Aktuell gibt es keine Kategorien zur Prüfung.</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modals -->
|
|
<div class="modal" id="modalAvatar">
|
|
<div class="panel panel--avatar">
|
|
<div class="head flex between center-y">
|
|
<h3>Profilbild gestalten</h3>
|
|
<button class="btn ghost" type="button" data-modal-close>✕</button>
|
|
</div>
|
|
<form method="post" class="avatar-builder" data-avatar-builder style="margin-top:12px;">
|
|
<input type="hidden" name="action" value="avatar_update">
|
|
<?php if (count($avatarStyleKeys) <= 1): ?>
|
|
<input type="hidden" name="avatar_style" value="<?= htmlspecialchars($currentAvatarStyle, ENT_QUOTES) ?>" data-avatar-style-select>
|
|
<?php endif; ?>
|
|
<input type="hidden" name="avatar_seed" value="<?= htmlspecialchars((string)$profile['avatar_seed'], ENT_QUOTES) ?>" data-avatar-seed>
|
|
<div class="avatar-builder__shell">
|
|
<div class="avatar-builder__top">
|
|
<div class="avatar-builder__hero">
|
|
<div class="avatar-builder__hero-preview" data-avatar-preview>
|
|
<?= \App\Avatar\AvatarManager::render($profile, $profile['display_name'] ?: 'Papa', 'xxl', $dashboardUserId) ?>
|
|
</div>
|
|
<div class="avatar-builder__hero-content">
|
|
<p class="muted small">Wähle eine Komponente und passe dein neues Profilbild Schritt für Schritt an. Gespeichert wird erst nach Klick auf den Button unten.</p>
|
|
<?php if (count($avatarStyleKeys) > 1): ?>
|
|
<div class="stack gap-6" style="max-width:320px;">
|
|
<label class="label" for="avatarStyleSelect">Stil</label>
|
|
<select id="avatarStyleSelect" name="avatar_style" class="select" data-avatar-style-select>
|
|
<?php foreach ($avatarBuilder as $styleKey => $styleMeta): ?>
|
|
<option value="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>" <?= $styleKey === $currentAvatarStyle ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars((string)$styleMeta['label'], ENT_QUOTES) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="flex gap-12 avatar-builder__actions" style="flex-wrap:wrap;">
|
|
<button class="btn ghost" type="button" data-avatar-random>Random Profilbild</button>
|
|
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
|
|
<button class="btn" type="submit">Profilbild speichern</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="avatar-builder__body">
|
|
<?php foreach ($avatarBuilder as $styleKey => $styleMeta): ?>
|
|
<?php $styleComponents = $styleMeta['components'] ?? []; ?>
|
|
<div class="avatar-style-group <?= $styleKey === $currentAvatarStyle ? 'is-active' : '' ?>" data-avatar-style-panel="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>" <?= $styleKey === $currentAvatarStyle ? '' : 'hidden' ?>>
|
|
<div class="avatar-component-nav" role="tablist" aria-label="Avatar-Komponenten">
|
|
<?php $firstField = array_key_first($styleComponents); ?>
|
|
<?php foreach ($styleComponents as $field => $component): ?>
|
|
<button
|
|
type="button"
|
|
class="avatar-component-nav__button <?= $field === $firstField ? 'is-active' : '' ?>"
|
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
|
data-avatar-component-tab="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
|
|
aria-selected="<?= $field === $firstField ? 'true' : 'false' ?>"
|
|
>
|
|
<?= htmlspecialchars((string)($component['label'] ?? $field), ENT_QUOTES) ?>
|
|
<span class="muted small">(<?= (int)($component['count'] ?? 0) ?>)</span>
|
|
</button>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="avatar-component-panels">
|
|
<?php foreach ($styleComponents as $field => $component): ?>
|
|
<section class="avatar-component-panel avatar-component-panel--<?= htmlspecialchars((string)$field, ENT_QUOTES) ?> <?= $field === $firstField ? 'is-active' : '' ?>" data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>" data-avatar-component-panel="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>">
|
|
<div class="avatar-component-panel__head">
|
|
<strong><?= htmlspecialchars((string)($component['label'] ?? $field), ENT_QUOTES) ?></strong>
|
|
<span class="muted small">Gesamtes Profilbild zur sicheren Vorschau</span>
|
|
</div>
|
|
<?php $componentColors = $component['colors'] ?? []; ?>
|
|
<?php if ($componentColors): ?>
|
|
<div class="avatar-component-colors">
|
|
<?php foreach ($componentColors as $colorField => $colorMeta): ?>
|
|
<?php
|
|
$colorValue = (string)($colorMeta['value'] ?? '');
|
|
$colorDefault = '#000000';
|
|
$samples = is_array($colorMeta['samples'] ?? null) ? $colorMeta['samples'] : [];
|
|
?>
|
|
<div class="avatar-color-control">
|
|
<label class="avatar-color-control__label" for="avatarColor<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"><?= htmlspecialchars((string)($colorMeta['label'] ?? $colorField), ENT_QUOTES) ?></label>
|
|
<input
|
|
type="hidden"
|
|
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>]"
|
|
value="<?= htmlspecialchars($colorValue, ENT_QUOTES) ?>"
|
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
|
data-avatar-color-input="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
|
>
|
|
<div class="avatar-color-control__row">
|
|
<input
|
|
id="avatarColor<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
|
|
type="color"
|
|
value="<?= htmlspecialchars($colorValue !== '' ? $colorValue : $colorDefault, ENT_QUOTES) ?>"
|
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
|
data-avatar-color-picker="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
|
data-avatar-color-default="<?= htmlspecialchars($colorDefault, ENT_QUOTES) ?>"
|
|
class="avatar-color-control__picker"
|
|
>
|
|
<button
|
|
class="btn ghost avatar-color-control__reset"
|
|
type="button"
|
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
|
data-avatar-color-reset="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
|
>Standard</button>
|
|
</div>
|
|
<?php if ($samples): ?>
|
|
<div class="avatar-color-control__samples">
|
|
<?php foreach ($samples as $sampleColor): ?>
|
|
<button
|
|
type="button"
|
|
class="avatar-color-sample"
|
|
title="<?= htmlspecialchars((string)$sampleColor, ENT_QUOTES) ?>"
|
|
aria-label="<?= htmlspecialchars((string)$sampleColor, ENT_QUOTES) ?>"
|
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
|
data-avatar-color-sample-field="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
|
data-avatar-color-sample-value="<?= htmlspecialchars((string)$sampleColor, ENT_QUOTES) ?>"
|
|
style="background: <?= htmlspecialchars((string)$sampleColor, ENT_QUOTES) ?>;"
|
|
></button>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="avatar-component-grid">
|
|
<?php foreach (($component['options'] ?? []) as $choice): ?>
|
|
<label class="avatar-component-card">
|
|
<input
|
|
type="radio"
|
|
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>]"
|
|
value="<?= htmlspecialchars((string)($choice['value'] ?? ''), ENT_QUOTES) ?>"
|
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
|
data-avatar-field="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
|
|
<?= !empty($choice['checked']) ? 'checked' : '' ?>
|
|
>
|
|
<span class="avatar-component-card__thumb">
|
|
<img
|
|
src="<?= htmlspecialchars($avatarPlaceholder, ENT_QUOTES) ?>"
|
|
alt="<?= htmlspecialchars((string)($choice['label'] ?? ''), ENT_QUOTES) ?>"
|
|
data-avatar-option-thumb
|
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
|
data-avatar-option-field="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>"
|
|
data-avatar-option-value="<?= htmlspecialchars((string)($choice['value'] ?? ''), ENT_QUOTES) ?>"
|
|
>
|
|
</span>
|
|
<span class="avatar-component-card__label"><?= htmlspecialchars((string)($choice['label'] ?? ''), ENT_QUOTES) ?></span>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</section>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="modalProfile">
|
|
<div class="panel">
|
|
<div class="head flex between center-y">
|
|
<h3>Profil bearbeiten</h3>
|
|
<button class="btn ghost" type="button" data-modal-close>✕</button>
|
|
</div>
|
|
<form method="post" class="stack gap-12 profile-edit-form">
|
|
<input type="hidden" name="action" value="profile">
|
|
<input type="hidden" id="pRegion" name="region" value="<?= htmlspecialchars((string)($profile['region'] ?? ''), ENT_QUOTES) ?>">
|
|
<input type="hidden" id="pLat" name="lat" value="<?= htmlspecialchars((string)($profile['lat'] ?? ''), ENT_QUOTES) ?>">
|
|
<input type="hidden" id="pLng" name="lng" value="<?= htmlspecialchars((string)($profile['lng'] ?? ''), ENT_QUOTES) ?>">
|
|
<p class="muted small" style="margin:0;">Persönliche Angaben werden app-seitig verschlüsselt gespeichert. Nur für Login und Kontofunktionen wird die E-Mail intern über einen separaten Lookup-Hash adressiert.</p>
|
|
<details class="profile-edit-section" open>
|
|
<summary>Basisdaten</summary>
|
|
<div class="profile-edit-section__body">
|
|
<div class="form-grid">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pName">Anzeigename</label>
|
|
<input id="pName" name="display_name" class="input" value="<?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?>">
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pEmail">E-Mail-Adresse</label>
|
|
<input id="pEmail" name="email" type="email" class="input" value="<?= htmlspecialchars((string)$profile['email'], ENT_QUOTES) ?>" autocomplete="email" required>
|
|
</div>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pFirst">Vorname</label>
|
|
<input id="pFirst" name="first_name" class="input" value="<?= htmlspecialchars($profile['first_name'], ENT_QUOTES) ?>" autocomplete="given-name">
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pLast">Nachname</label>
|
|
<input id="pLast" name="last_name" class="input" value="<?= htmlspecialchars($profile['last_name'], ENT_QUOTES) ?>" autocomplete="family-name">
|
|
</div>
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pPhone">Telefon (mobil)</label>
|
|
<input id="pPhone" name="contact_phone" class="input" value="<?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?>" autocomplete="tel">
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<details class="profile-edit-section" open>
|
|
<summary>Adresse und Umgebung</summary>
|
|
<div class="profile-edit-section__body">
|
|
<div class="form-grid">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pStreet">Straße / Hausnummer</label>
|
|
<input id="pStreet" name="street" class="input" value="<?= htmlspecialchars((string)($profile['street'] ?? ''), ENT_QUOTES) ?>" placeholder="z. B. Musterstraße 12" autocomplete="street-address">
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pZip">PLZ</label>
|
|
<input id="pZip" name="zip" class="input" value="<?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?>" autocomplete="postal-code">
|
|
</div>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pCity">Ort</label>
|
|
<input id="pCity" name="city" class="input" value="<?= htmlspecialchars($profile['city'], ENT_QUOTES) ?>" autocomplete="address-level2">
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pRegionView">Region / Bezirk</label>
|
|
<input id="pRegionView" class="input" value="<?= htmlspecialchars((string)($profile['region'] ?? ''), ENT_QUOTES) ?>" placeholder="wird bei Lokalisierung ergänzt" autocomplete="address-level1">
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-12" style="flex-wrap:wrap;">
|
|
<button class="btn ghost" type="button" id="btnProfileLocate">Adresse suchen</button>
|
|
<button class="btn ghost" type="button" id="btnProfileBrowserLocation">Browser-Adresse übernehmen</button>
|
|
<button class="btn ghost" type="button" data-modal-open="modalProfileAddressHelp" aria-label="Hilfe zur Profiladresse">?</button>
|
|
</div>
|
|
<p class="muted small" style="margin:0;">Für Suche und Browser-Übernahme werden dieselben Karten-/Adressdienste wie bei Events genutzt und nur nach Einwilligung in externe Dienste.</p>
|
|
<div id="profileAddressStatus" class="muted small" style="margin:0;"></div>
|
|
<div id="profileAddressSelectedLabel" class="muted small" style="margin:0;<?= empty($profile['street']) && empty($profile['city']) ? ' display:none;' : '' ?>">
|
|
<?php if (!empty($profile['street']) || !empty($profile['city'])): ?>
|
|
Gewählte Adresse: <?= htmlspecialchars(trim(implode(', ', array_filter([(string)($profile['street'] ?? ''), trim((string)$profile['zip'] . ' ' . (string)$profile['city']), (string)($profile['region'] ?? '')]))), ENT_QUOTES) ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div id="profileAddressNeedsValidation" class="muted small" style="margin:0; display:none;">
|
|
Die Adresse wurde manuell geändert. Vor dem Speichern bitte erneut suchen oder die Browser-Adresse übernehmen.
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<details class="profile-edit-section">
|
|
<summary>Sprache und Beruf</summary>
|
|
<div class="profile-edit-section__body">
|
|
<div class="stack gap-6">
|
|
<label class="label">Sprachen (Mehrfachauswahl)</label>
|
|
<div class="chips" style="flex-wrap: wrap;">
|
|
<?php
|
|
$langOptions = ['Deutsch','Englisch','Französisch','Spanisch','Türkisch','Arabisch','Polnisch'];
|
|
$currentLangs = array_filter(array_map('trim', explode(',', (string)$profile['languages'])));
|
|
?>
|
|
<?php foreach ($langOptions as $opt): ?>
|
|
<label class="chip" style="cursor:pointer;">
|
|
<input type="checkbox" name="languages[]" value="<?= htmlspecialchars($opt, ENT_QUOTES) ?>" <?= in_array($opt, $currentLangs, true) ? 'checked' : '' ?> style="margin-right:6px;">
|
|
<?= htmlspecialchars($opt, ENT_QUOTES) ?>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<label class="label" for="pLangCustom">Weitere Sprachen (kommagetrennt)</label>
|
|
<input id="pLangCustom" name="languages[]" class="input" placeholder="z. B. Italienisch, Niederländisch">
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pProf">Beruf</label>
|
|
<input id="pProf" name="profession" class="input" value="<?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?>" autocomplete="organization-title">
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<details class="profile-edit-section">
|
|
<summary>Kurzvorstellung</summary>
|
|
<div class="profile-edit-section__body">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="pAbout">Über dich</label>
|
|
<textarea id="pAbout" name="about" class="textarea" rows="4"><?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></textarea>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<div class="flex gap-12">
|
|
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
|
|
<button class="btn" type="submit">Speichern</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="modalProfileAddressHelp">
|
|
<div class="panel">
|
|
<div class="head flex between center-y">
|
|
<h3>Warum wir die Adresse abfragen</h3>
|
|
<button class="btn ghost" type="button" data-modal-close>✕</button>
|
|
</div>
|
|
<div class="stack gap-12">
|
|
<p class="muted" style="margin:0;">Deine Adresse hilft uns, dir passende Events, Termine und Treffen in deiner Umgebung gezielter anzuzeigen.</p>
|
|
<p class="muted" style="margin:0;">Wenn keine Adresse hinterlegt ist, können wir lokale Bedarfe und passende Angebote deutlich schlechter einschätzen.</p>
|
|
<p class="muted small" style="margin:0;">Die Adresse wird nur für diese Plattform-Funktionen genutzt. Straße und Hausnummer werden verschlüsselt gespeichert.</p>
|
|
<div class="flex gap-12">
|
|
<button class="btn" type="button" data-modal-close>Verstanden</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="modalProfileAddressResults">
|
|
<div class="panel">
|
|
<div class="head flex between center-y">
|
|
<h3>Adresse auswählen</h3>
|
|
<button class="btn ghost" type="button" data-modal-close>✕</button>
|
|
</div>
|
|
<div class="stack gap-12">
|
|
<p class="muted small" style="margin:0;">Wähle den passenden Treffer für dein Profil.</p>
|
|
<div id="profileAddressResults" class="stack gap-12"></div>
|
|
<div class="flex gap-12">
|
|
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="modalChild">
|
|
<div class="panel">
|
|
<div class="head flex between center-y">
|
|
<h3><?= htmlspecialchars($childModalTitle, ENT_QUOTES) ?></h3>
|
|
<button class="btn ghost" type="button" data-modal-close>✕</button>
|
|
</div>
|
|
<form method="post" class="stack gap-12">
|
|
<input type="hidden" name="action" value="<?= htmlspecialchars($childAction, ENT_QUOTES) ?>">
|
|
<?php if ($editingChild): ?>
|
|
<input type="hidden" name="child_id" value="<?= (int)$editChild['id'] ?>">
|
|
<?php endif; ?>
|
|
<div class="form-grid">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="cName">Vorname</label>
|
|
<input id="cName" name="first_name" class="input" value="<?= htmlspecialchars((string)($editChild['first_name'] ?? ''), ENT_QUOTES) ?>" required>
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="cGender">Geschlecht</label>
|
|
<select id="cGender" name="gender" class="select">
|
|
<option value="male" <?= (($editChild['gender'] ?? 'unknown') === 'male') ? 'selected' : '' ?>>Männlich</option>
|
|
<option value="female" <?= (($editChild['gender'] ?? 'unknown') === 'female') ? 'selected' : '' ?>>Weiblich</option>
|
|
<option value="diverse" <?= (($editChild['gender'] ?? 'unknown') === 'diverse') ? 'selected' : '' ?>>Divers</option>
|
|
<option value="unknown" <?= (($editChild['gender'] ?? 'unknown') === 'unknown') ? 'selected' : '' ?>>Unbekannt</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="cBirth">Geburtsdatum</label>
|
|
<input id="cBirth" name="birthdate" class="input" type="date" value="<?= htmlspecialchars((string)($editChild['birthdate'] ?? ''), ENT_QUOTES) ?>">
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="cAge">Alter (Jahre)</label>
|
|
<input id="cAge" name="age_years" class="input" type="number" min="0" max="18" value="<?= htmlspecialchars((string)($editChild['age_years'] ?? ''), ENT_QUOTES) ?>">
|
|
<p class="muted small" style="margin:0;">Mit Geburtsdatum wird das Alter automatisch berechnet und jährlich aktualisiert. Ohne Geburtsdatum gilt der manuelle Wert.</p>
|
|
</div>
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="cNote">Notiz</label>
|
|
<input id="cNote" name="note" class="input" value="<?= htmlspecialchars((string)($editChild['note'] ?? ''), ENT_QUOTES) ?>">
|
|
</div>
|
|
<div class="flex gap-12">
|
|
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
|
|
<button class="btn" type="submit"><?= $editingChild ? 'Änderungen speichern' : 'Speichern' ?></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="modalEvent">
|
|
<div class="panel">
|
|
<div class="head flex between center-y">
|
|
<h3><?= htmlspecialchars($eventTitle, ENT_QUOTES) ?></h3>
|
|
<button class="btn ghost" type="button" data-modal-close>✕</button>
|
|
</div>
|
|
<form class="stack gap-12" style="margin-top: 10px;" method="post" action="/dashboard?section=events#events" enctype="multipart/form-data">
|
|
<input type="hidden" name="action" value="<?= htmlspecialchars($actionEvent, ENT_QUOTES) ?>">
|
|
<input type="hidden" name="entry_kind" id="entryKind" value="<?= htmlspecialchars($currentEntryKind, ENT_QUOTES) ?>">
|
|
<?php if ($editingEvent && $editEvent): ?>
|
|
<input type="hidden" name="event_id" value="<?= (int)$editEvent['id'] ?>">
|
|
<?php endif; ?>
|
|
<?php if ($editingListing && $editListing): ?>
|
|
<input type="hidden" name="listing_id" value="<?= (int)$editListing['id'] ?>">
|
|
<?php endif; ?>
|
|
<datalist id="existingPlacesList">
|
|
<?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) ?>'>
|
|
<?= htmlspecialchars(trim(implode(', ', array_filter([(string)($placeSuggestion['street'] ?? ''), trim((string)($placeSuggestion['zip'] ?? '') . ' ' . (string)($placeSuggestion['city'] ?? '')), (string)($placeSuggestion['region'] ?? '')]))), ENT_QUOTES) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</datalist>
|
|
<?php if (!$editing): ?>
|
|
<div class="stack gap-8">
|
|
<label class="label">Was möchtest du anlegen?</label>
|
|
<div class="chips" style="flex-wrap:wrap;">
|
|
<button class="btn ghost" type="button" data-entry-kind-choice="event">Eigenes Event</button>
|
|
<button class="btn ghost" type="button" data-entry-kind-choice="place">Ort</button>
|
|
<button class="btn ghost" type="button" data-entry-kind-choice="editorial_event">Sonstige Veranstaltung</button>
|
|
</div>
|
|
<p class="muted small" id="entryKindHint" style="margin:0;">Eigenes Event: von dir organisiert und mit konkretem Termin.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="card">
|
|
<strong>Eintragstyp</strong>
|
|
<p class="muted small" style="margin:8px 0 0;">
|
|
<?= $currentEntryKind === 'place' ? 'Ort: dauerhaft verfügbar.' : ($currentEntryKind === 'editorial_event' ? 'Sonstige Veranstaltung: zeitlich begrenzter Hinweis.' : 'Eigenes Event: von dir organisiert.') ?>
|
|
</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="evTitle">Titel</label>
|
|
<input id="evTitle" name="title" class="input" placeholder="z. B. Väter-Kaffee im Park" required value="<?= htmlspecialchars($titleValue, ENT_QUOTES) ?>">
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<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>
|
|
<p class="muted small" style="margin:0;">Die Kurzbeschreibung wird automatisch aus dem Text erzeugt.</p>
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="categoryInput">Kategorie</label>
|
|
<input id="categoryInput" name="category_input" class="input" list="listingCategoryList" placeholder="z. B. Spielplatz, Café, Zirkus" value="<?= htmlspecialchars($categoryInputValue, ENT_QUOTES) ?>">
|
|
<input type="hidden" id="listingCategorySlug" name="category_slug" value="<?= htmlspecialchars($categorySlugValue, ENT_QUOTES) ?>">
|
|
<datalist id="listingCategoryList">
|
|
<?php foreach ($listingCategories as $category): ?>
|
|
<option value="<?= htmlspecialchars((string)$category['title'], ENT_QUOTES) ?>"></option>
|
|
<?php endforeach; ?>
|
|
</datalist>
|
|
<div id="categoryInputStatus" class="muted small" style="margin:0;"></div>
|
|
</div>
|
|
<div class="form-grid" id="eventDateRow">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="evDate" id="eventDateLabel">Datum</label>
|
|
<input id="evDate" name="starts_at" class="input" type="date" required value="<?= htmlspecialchars($startVal, ENT_QUOTES) ?>">
|
|
</div>
|
|
<div class="stack gap-6" id="eventChildrenWrap">
|
|
<label class="label" for="withChildren">Mit Kindern</label>
|
|
<select id="withChildren" name="with_children" class="select">
|
|
<option value="yes" <?= $withChildrenValue === 'yes' ? 'selected' : '' ?>>Ja</option>
|
|
<option value="no" <?= $withChildrenValue === 'no' ? 'selected' : '' ?>>Nein</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-grid" id="eventMetaRow">
|
|
<div class="stack gap-6" id="eventCapacityWrap">
|
|
<label class="label" for="maxParticipants">Platzzahl (optional)</label>
|
|
<input id="maxParticipants" name="max_participants" class="input" type="number" min="1" value="<?= htmlspecialchars($capacityValue, ENT_QUOTES) ?>" placeholder="leer = offen">
|
|
</div>
|
|
<div class="stack gap-6" id="eventValidUntilWrap">
|
|
<label class="label" for="recurrenceUntil">Gültig bis</label>
|
|
<input id="recurrenceUntil" name="recurrence_until" class="input" type="date" value="<?= htmlspecialchars($recurrenceUntilValue, ENT_QUOTES) ?>">
|
|
</div>
|
|
</div>
|
|
<div class="stack gap-6" id="eventRecurrenceWrap">
|
|
<label class="label" for="recurrenceMode">Wiederholung</label>
|
|
<select id="recurrenceMode" name="recurrence_mode" class="select">
|
|
<option value="single" <?= $recurrenceModeValue === 'single' ? 'selected' : '' ?>>Einmalig</option>
|
|
<option value="daily" <?= $recurrenceModeValue === 'daily' ? 'selected' : '' ?>>Täglich</option>
|
|
<option value="weekly" <?= $recurrenceModeValue === 'weekly' ? 'selected' : '' ?>>Wöchentlich</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="evZip">PLZ</label>
|
|
<input id="evZip" name="zip" class="input" maxlength="5" value="<?= htmlspecialchars($zipValue, ENT_QUOTES) ?>">
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="evCity">Stadt</label>
|
|
<input id="evCity" name="city" class="input" value="<?= htmlspecialchars($cityValue, ENT_QUOTES) ?>">
|
|
</div>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="stack gap-6">
|
|
<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) ?>">
|
|
<p class="muted small">Wird zur Karten-/Umkreissuche genutzt.</p>
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="evRegion">Region/Bezirk</label>
|
|
<input id="evRegion" name="region" class="input" value="<?= htmlspecialchars($regionValue, ENT_QUOTES) ?>">
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-8" style="flex-wrap:wrap; align-items:center;">
|
|
<button class="btn ghost" type="button" id="btnEventAddressSearch">Adresse suchen</button>
|
|
<button class="btn ghost" type="button" id="btnEventBrowserLocation">📍 Aktuellen Standort</button>
|
|
</div>
|
|
<div id="eventAddressStatus" class="muted small" style="margin:0;"></div>
|
|
<input type="hidden" id="evLat" name="lat" value="<?= htmlspecialchars($latValue, ENT_QUOTES) ?>">
|
|
<input type="hidden" id="evLng" name="lng" value="<?= htmlspecialchars($lngValue, ENT_QUOTES) ?>">
|
|
<details class="card" id="mapDetails">
|
|
<summary style="cursor:pointer; font-weight:600;">Adresse auf Karte wählen</summary>
|
|
<div class="stack gap-6" style="margin-top:12px;">
|
|
<div class="flex gap-8" style="flex-wrap:wrap; align-items:center;">
|
|
<button class="btn ghost" type="button" id="btnEventMapCurrentLocation">📍 Aktuellen Standort auf Karte zeigen</button>
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<button class="btn ghost" type="button" id="btnMap">Auf Karte suchen</button>
|
|
<div id="mapWrapper" class="map-wrapper" hidden>
|
|
<div class="form-row">
|
|
<label class="label" for="mapSearch">Adresse suchen (optional)</label>
|
|
<div class="flex gap-8">
|
|
<input id="mapSearch" class="input" placeholder="Straße, PLZ, Ort">
|
|
<button class="btn ghost" type="button" id="btnMapSearch">Suchen</button>
|
|
</div>
|
|
<p class="muted small">Klick auf die Karte setzt den Treffpunkt. Zoom/Scroll möglich.</p>
|
|
</div>
|
|
<div id="mapContainer" class="map-container"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<details class="card" id="placeContactWrap">
|
|
<summary style="cursor:pointer; font-weight:600;">Kontaktangaben</summary>
|
|
<div class="form-grid" style="margin-top:12px;">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="placePhone">Telefon (optional)</label>
|
|
<input id="placePhone" name="phone" class="input" value="<?= htmlspecialchars($phoneValue, ENT_QUOTES) ?>">
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="placeWebsite">Webseite (optional)</label>
|
|
<input id="placeWebsite" name="website_url" class="input" type="url" placeholder="https://..." value="<?= htmlspecialchars($websiteValue, ENT_QUOTES) ?>">
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<details class="card" id="placeOpeningHoursWrap">
|
|
<summary style="cursor:pointer; font-weight:600;">Öffnungszeiten / Zeitfenster</summary>
|
|
<div class="stack gap-6" style="margin-top:12px;">
|
|
<div class="flex between center-y">
|
|
<label class="label" for="openingHoursNote">Öffnungszeiten / Zeitfenster</label>
|
|
<button class="btn ghost" type="button" id="btnAddOpeningRow">Zeit hinzufügen</button>
|
|
</div>
|
|
<div id="openingHoursRows" class="stack gap-8">
|
|
<?php foreach ($openingHoursRows as $openingRow): ?>
|
|
<div class="form-grid opening-row">
|
|
<input name="opening_day_label[]" class="input" placeholder="z. B. Montag oder täglich" value="<?= htmlspecialchars((string)($openingRow['day_label'] ?? ''), ENT_QUOTES) ?>">
|
|
<input name="opening_time_from[]" class="input" type="time" value="<?= htmlspecialchars((string)($openingRow['time_from'] ?? ''), ENT_QUOTES) ?>">
|
|
<input name="opening_time_to[]" class="input" type="time" value="<?= htmlspecialchars((string)($openingRow['time_to'] ?? ''), ENT_QUOTES) ?>">
|
|
<input name="opening_note[]" class="input" placeholder="Hinweis" value="<?= htmlspecialchars((string)($openingRow['note'] ?? ''), ENT_QUOTES) ?>">
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<textarea id="openingHoursNote" name="opening_hours_note" class="textarea" rows="2" placeholder="Freitext-Hinweis zu Öffnungszeiten oder Besonderheiten"><?= htmlspecialchars($openingHoursValue, ENT_QUOTES) ?></textarea>
|
|
</div>
|
|
</details>
|
|
<details class="card" id="visibilityDetails">
|
|
<summary style="cursor:pointer; font-weight:600;">Weitere Optionen</summary>
|
|
<div class="form-grid" style="margin-top:12px;">
|
|
<div class="stack gap-6">
|
|
<label class="label" for="evVis">Sichtbarkeit</label>
|
|
<select id="evVis" name="visibility" class="select">
|
|
<option value="public" <?= $visibilityValue === 'public' ? 'selected' : '' ?>>Öffentlich</option>
|
|
<option value="members" <?= $visibilityValue === 'members' ? 'selected' : '' ?>>Nur Mitglieder</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<details class="card">
|
|
<summary style="cursor:pointer; font-weight:600;">Bild hinzufügen</summary>
|
|
<div class="stack gap-6" style="margin-top:12px;">
|
|
<label class="label" for="imageFile">Bild (optional)</label>
|
|
<input id="imageFile" name="image_file" class="input" type="file" accept="image/jpeg,image/png,image/webp">
|
|
<?php if ($imagePathValue !== ''): ?>
|
|
<div class="muted small">Aktuelles Bild: <a href="<?= htmlspecialchars($imagePathValue, ENT_QUOTES) ?>" target="_blank" rel="noopener">Ansehen</a></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</details>
|
|
<details class="card" id="eventPricingWrap">
|
|
<summary style="cursor:pointer; font-weight:600;">Eintritt / Kosten</summary>
|
|
<div class="stack gap-10" style="margin-top:12px;">
|
|
<div class="flex between center-y">
|
|
<label class="label">Preisregeln</label>
|
|
<button class="btn ghost" type="button" id="btnAddPriceRow">Preisregel hinzufügen</button>
|
|
</div>
|
|
<div id="priceRows" class="stack gap-8">
|
|
<?php foreach ($priceRows as $priceRow): ?>
|
|
<div class="stack gap-8 price-row card">
|
|
<div class="form-grid">
|
|
<input name="price_age_from[]" class="input" type="number" min="0" max="99" placeholder="Alter von" value="<?= htmlspecialchars((string)($priceRow['age_from'] ?? ''), ENT_QUOTES) ?>">
|
|
<input name="price_age_to[]" class="input" type="number" min="0" max="99" placeholder="Alter bis" value="<?= htmlspecialchars((string)($priceRow['age_to'] ?? ''), ENT_QUOTES) ?>">
|
|
</div>
|
|
<div class="form-grid">
|
|
<input name="price_amount[]" class="input" inputmode="decimal" placeholder="Preis" value="<?= htmlspecialchars((string)($priceRow['amount'] ?? ''), ENT_QUOTES) ?>">
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="stack gap-6">
|
|
<label class="label" for="specialConditionsNote">Sonderkonditionen</label>
|
|
<textarea id="specialConditionsNote" name="special_conditions_note" class="textarea" rows="2" placeholder="z. B. Sonderkonditionen für Senioren oder Menschen mit Behinderung vorhanden"><?= htmlspecialchars($specialConditionsValue, ENT_QUOTES) ?></textarea>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<div class="flex gap-12">
|
|
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
|
|
<button class="btn" type="submit"><?= htmlspecialchars($submitLabel, ENT_QUOTES) ?></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
document.querySelectorAll('[data-modal-open]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const id = btn.getAttribute('data-modal-open');
|
|
const modal = document.getElementById(id);
|
|
if (modal) modal.classList.add('open');
|
|
});
|
|
});
|
|
document.querySelectorAll('[data-modal-close]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const modal = btn.closest('.modal');
|
|
if (modal) modal.classList.remove('open');
|
|
});
|
|
});
|
|
document.querySelectorAll('.modal').forEach(modal => {
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) modal.classList.remove('open');
|
|
});
|
|
});
|
|
|
|
// Map picker for events (Leaflet)
|
|
const btnMap = document.getElementById('btnMap');
|
|
const mapWrapper = document.getElementById('mapWrapper');
|
|
const mapContainer = document.getElementById('mapContainer');
|
|
const btnMapSearch = document.getElementById('btnMapSearch');
|
|
const btnEventMapCurrentLocation = document.getElementById('btnEventMapCurrentLocation');
|
|
const btnEventAddressSearch = document.getElementById('btnEventAddressSearch');
|
|
const btnEventBrowserLocation = document.getElementById('btnEventBrowserLocation');
|
|
const eventAddressStatus = document.getElementById('eventAddressStatus');
|
|
const mapSearch = document.getElementById('mapSearch');
|
|
const latInput = document.getElementById('evLat');
|
|
const lngInput = document.getElementById('evLng');
|
|
const entryKindInput = document.getElementById('entryKind');
|
|
const entryKindHint = document.getElementById('entryKindHint');
|
|
const entryKindButtons = document.querySelectorAll('[data-entry-kind-choice]');
|
|
const eventDateRow = document.getElementById('eventDateRow');
|
|
const eventDateLabel = document.getElementById('eventDateLabel');
|
|
const eventChildrenWrap = document.getElementById('eventChildrenWrap');
|
|
const eventCapacityWrap = document.getElementById('eventCapacityWrap');
|
|
const eventValidUntilWrap = document.getElementById('eventValidUntilWrap');
|
|
const eventPricingWrap = document.getElementById('eventPricingWrap');
|
|
const visibilityDetails = document.getElementById('visibilityDetails');
|
|
const placeContactWrap = document.getElementById('placeContactWrap');
|
|
const placeOpeningHoursWrap = document.getElementById('placeOpeningHoursWrap');
|
|
const eventRecurrenceWrap = document.getElementById('eventRecurrenceWrap');
|
|
const eventDateInput = document.getElementById('evDate');
|
|
const categoryInput = document.getElementById('categoryInput');
|
|
const categorySlugInput = document.getElementById('listingCategorySlug');
|
|
const categoryInputStatus = document.getElementById('categoryInputStatus');
|
|
const recurrenceModeSelect = document.getElementById('recurrenceMode');
|
|
const openingHoursRows = document.getElementById('openingHoursRows');
|
|
const priceRows = document.getElementById('priceRows');
|
|
const btnAddOpeningRow = document.getElementById('btnAddOpeningRow');
|
|
const btnAddPriceRow = document.getElementById('btnAddPriceRow');
|
|
const streetInput = document.getElementById('evStreet');
|
|
const zipInput = document.getElementById('evZip');
|
|
const cityInput = document.getElementById('evCity');
|
|
const regionInput = document.getElementById('evRegion');
|
|
const profileLocateButton = document.getElementById('btnProfileLocate');
|
|
const profileBrowserLocationButton = document.getElementById('btnProfileBrowserLocation');
|
|
const profileStreetInput = document.getElementById('pStreet');
|
|
const profileZipInput = document.getElementById('pZip');
|
|
const profileCityInput = document.getElementById('pCity');
|
|
const profileRegionInput = document.getElementById('pRegion');
|
|
const profileRegionViewInput = document.getElementById('pRegionView');
|
|
const profileLatInput = document.getElementById('pLat');
|
|
const profileLngInput = document.getElementById('pLng');
|
|
const profileAddressStatus = document.getElementById('profileAddressStatus');
|
|
const profileAddressSelectedLabel = document.getElementById('profileAddressSelectedLabel');
|
|
const profileAddressNeedsValidation = document.getElementById('profileAddressNeedsValidation');
|
|
const profileAddressResults = document.getElementById('profileAddressResults');
|
|
const profileAddressResultsModal = document.getElementById('modalProfileAddressResults');
|
|
let map, marker;
|
|
const existingCategories = <?= json_encode($listingCategories ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
let lastAutoGeocodeQuery = '';
|
|
|
|
function normalizeGeoResult(result) {
|
|
const address = result.address || {};
|
|
const street = [address.road || '', address.house_number || ''].filter(Boolean).join(' ').trim();
|
|
const region = address.city_district || address.suburb || address.state || address.county || address.region || address.state_district || '';
|
|
const city = address.city || address.town || address.village || '';
|
|
|
|
return {
|
|
label: result.display_name || '',
|
|
street,
|
|
zip: address.postcode || '',
|
|
city,
|
|
region,
|
|
lat: parseFloat(result.lat),
|
|
lng: parseFloat(result.lon),
|
|
};
|
|
}
|
|
|
|
function setSectionState(element, visible) {
|
|
if (!element) return;
|
|
element.hidden = !visible;
|
|
element.querySelectorAll('input, select, textarea, button').forEach((field) => {
|
|
if (field.type === 'hidden') return;
|
|
field.disabled = !visible;
|
|
});
|
|
}
|
|
|
|
function syncEventEntryKind(nextKind) {
|
|
const kind = nextKind || entryKindInput?.value || 'event';
|
|
if (entryKindInput) entryKindInput.value = kind;
|
|
|
|
entryKindButtons.forEach((button) => {
|
|
const active = button.getAttribute('data-entry-kind-choice') === kind;
|
|
button.className = active ? 'btn' : 'btn ghost';
|
|
button.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
});
|
|
|
|
if (entryKindHint) {
|
|
if (kind === 'place') {
|
|
entryKindHint.textContent = 'Ort: dauerhaft verfügbar, zum Beispiel Spielplatz, Café, Restaurant oder Zoo.';
|
|
} else if (kind === 'editorial_event') {
|
|
entryKindHint.textContent = 'Sonstige Veranstaltung: zeitlich begrenzter Hinweis, zum Beispiel Zirkus oder Hüpfburgenstadt.';
|
|
} else {
|
|
entryKindHint.textContent = 'Eigenes Event: von dir organisiert und mit konkretem Termin.';
|
|
}
|
|
}
|
|
|
|
setSectionState(eventDateRow, kind !== 'place');
|
|
if (eventDateInput) eventDateInput.required = kind !== 'place';
|
|
if (categoryInput) categoryInput.required = true;
|
|
setSectionState(eventChildrenWrap, kind === 'event' || kind === 'editorial_event');
|
|
setSectionState(eventCapacityWrap, kind === 'event');
|
|
setSectionState(eventValidUntilWrap, kind === 'editorial_event');
|
|
setSectionState(eventPricingWrap, kind === 'place' || kind === 'editorial_event');
|
|
setSectionState(placeContactWrap, kind === 'place' || kind === 'editorial_event');
|
|
setSectionState(placeOpeningHoursWrap, kind === 'place' || kind === 'editorial_event');
|
|
setSectionState(eventRecurrenceWrap, kind === 'editorial_event');
|
|
setSectionState(visibilityDetails, kind === 'event');
|
|
if (eventDateLabel) {
|
|
eventDateLabel.textContent = kind === 'editorial_event' ? 'Gültig von' : 'Datum';
|
|
}
|
|
}
|
|
|
|
entryKindButtons.forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
syncEventEntryKind(button.getAttribute('data-entry-kind-choice') || 'event');
|
|
});
|
|
});
|
|
syncEventEntryKind(entryKindInput?.value || 'event');
|
|
|
|
function createOpeningRow() {
|
|
const row = document.createElement('div');
|
|
row.className = 'form-grid opening-row';
|
|
row.innerHTML = `
|
|
<input name="opening_day_label[]" class="input" placeholder="z. B. Montag oder täglich">
|
|
<input name="opening_time_from[]" class="input" type="time">
|
|
<input name="opening_time_to[]" class="input" type="time">
|
|
<input name="opening_note[]" class="input" placeholder="Hinweis">
|
|
`;
|
|
openingHoursRows?.appendChild(row);
|
|
}
|
|
|
|
function createPriceRow() {
|
|
const row = document.createElement('div');
|
|
row.className = 'stack gap-8 price-row card';
|
|
row.innerHTML = `
|
|
<div class="form-grid">
|
|
<input name="price_age_from[]" class="input" type="number" min="0" max="99" placeholder="Alter von">
|
|
<input name="price_age_to[]" class="input" type="number" min="0" max="99" placeholder="Alter bis">
|
|
</div>
|
|
<div class="form-grid">
|
|
<input name="price_amount[]" class="input" inputmode="decimal" placeholder="Preis">
|
|
</div>
|
|
`;
|
|
priceRows?.appendChild(row);
|
|
}
|
|
|
|
btnAddOpeningRow?.addEventListener('click', createOpeningRow);
|
|
btnAddPriceRow?.addEventListener('click', createPriceRow);
|
|
|
|
function syncCategoryInputState() {
|
|
const value = (categoryInput?.value || '').trim().toLowerCase();
|
|
if (!categorySlugInput || !categoryInputStatus) return;
|
|
if (!value) {
|
|
categorySlugInput.value = '';
|
|
categoryInputStatus.textContent = '';
|
|
return;
|
|
}
|
|
const match = existingCategories.find((category) => {
|
|
const title = String(category.title || '').trim().toLowerCase();
|
|
const slug = String(category.slug || '').trim().toLowerCase();
|
|
return value === title || value === slug;
|
|
});
|
|
if (match) {
|
|
categorySlugInput.value = String(match.slug || '');
|
|
categoryInputStatus.textContent = 'Bestehende Kategorie wird verwendet.';
|
|
return;
|
|
}
|
|
categorySlugInput.value = '';
|
|
categoryInputStatus.textContent = 'Wird neu angelegt.';
|
|
}
|
|
|
|
categoryInput?.addEventListener('input', syncCategoryInputState);
|
|
syncCategoryInputState();
|
|
|
|
function ensureLeaflet(callback) {
|
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
|
alert('Für Karten und Adresssuche bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
|
window.PKTConsent?.openPreferences?.();
|
|
return;
|
|
}
|
|
if (window.L) { callback(); return; }
|
|
const css = document.createElement('link');
|
|
css.rel = 'stylesheet';
|
|
css.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
|
|
document.head.appendChild(css);
|
|
const script = document.createElement('script');
|
|
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
|
script.onload = callback;
|
|
document.body.appendChild(script);
|
|
}
|
|
|
|
function updateAddressFields(addr) {
|
|
if (!addr) return;
|
|
if (streetInput) {
|
|
const street = [addr.road || '', addr.house_number || ''].filter(Boolean).join(' ').trim();
|
|
if (street) streetInput.value = street;
|
|
}
|
|
if (zipInput && addr.postcode) zipInput.value = addr.postcode;
|
|
if (cityInput && (addr.city || addr.town || addr.village)) cityInput.value = addr.city || addr.town || addr.village;
|
|
if (regionInput && (addr.suburb || addr.state || addr.county)) regionInput.value = addr.suburb || addr.state || addr.county;
|
|
}
|
|
|
|
function setLatLngInputs(latlng) {
|
|
latInput.value = latlng.lat.toFixed(7);
|
|
lngInput.value = latlng.lng.toFixed(7);
|
|
}
|
|
|
|
function setMarker(latlng, center = true) {
|
|
if (!map) return;
|
|
if (marker) {
|
|
marker.setLatLng(latlng);
|
|
} else {
|
|
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
|
marker.on('dragend', () => {
|
|
const ll = marker.getLatLng();
|
|
setLatLngInputs(ll);
|
|
reverseGeocode(ll.lat, ll.lng);
|
|
});
|
|
}
|
|
setLatLngInputs(latlng);
|
|
if (center) {
|
|
map.setView(latlng, Math.max(map.getZoom(), 14));
|
|
}
|
|
}
|
|
|
|
function reverseGeocode(lat, lng) {
|
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) return;
|
|
fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lng)}`, {
|
|
headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' },
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => updateAddressFields(data.address))
|
|
.catch(() => {});
|
|
}
|
|
|
|
function geocodeAndPlace(query) {
|
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
|
alert('Für Karten und Adresssuche bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
|
window.PKTConsent?.openPreferences?.();
|
|
return;
|
|
}
|
|
fetch('https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=' + encodeURIComponent(query), {
|
|
headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' },
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (Array.isArray(data) && data[0]) {
|
|
const lat = parseFloat(data[0].lat);
|
|
const lng = parseFloat(data[0].lon);
|
|
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
|
|
if (latInput) latInput.value = lat.toFixed(7);
|
|
if (lngInput) lngInput.value = lng.toFixed(7);
|
|
}
|
|
ensureLeaflet(() => {
|
|
mapWrapper.hidden = false;
|
|
initMap();
|
|
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
|
|
setMarker({ lat, lng });
|
|
updateAddressFields(data[0].address);
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function geocodeAddressCandidates(query, onSuccess) {
|
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
|
alert('Für Karten und Adresssuche bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
|
window.PKTConsent?.openPreferences?.();
|
|
return;
|
|
}
|
|
fetch('https://nominatim.openstreetmap.org/search?format=jsonv2&limit=5&addressdetails=1&q=' + encodeURIComponent(query), {
|
|
headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' },
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (Array.isArray(data) && data.length) {
|
|
onSuccess?.(data);
|
|
return;
|
|
}
|
|
if (profileAddressStatus) profileAddressStatus.textContent = '';
|
|
alert('Adresse konnte nicht eindeutig lokalisiert werden.');
|
|
})
|
|
.catch(() => {
|
|
if (profileAddressStatus) profileAddressStatus.textContent = '';
|
|
alert('Adresssuche aktuell nicht erreichbar.');
|
|
});
|
|
}
|
|
|
|
function reverseGeocodeProfileAddress(lat, lng, onSuccess) {
|
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
|
alert('Für Karten und Adresssuche bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
|
window.PKTConsent?.openPreferences?.();
|
|
return;
|
|
}
|
|
fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&addressdetails=1&lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lng)}`, {
|
|
headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' },
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data && data.lat && data.lon) {
|
|
onSuccess?.(data);
|
|
return;
|
|
}
|
|
if (profileAddressStatus) profileAddressStatus.textContent = '';
|
|
alert('Die Browser-Position konnte nicht in eine Adresse umgewandelt werden.');
|
|
})
|
|
.catch(() => {
|
|
if (profileAddressStatus) profileAddressStatus.textContent = '';
|
|
alert('Adresssuche aktuell nicht erreichbar.');
|
|
});
|
|
}
|
|
|
|
function initMap() {
|
|
if (map) { map.invalidateSize(); return; }
|
|
map = L.map(mapContainer).setView([51.1657, 10.4515], 6);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© OpenStreetMap',
|
|
}).addTo(map);
|
|
|
|
map.on('click', (e) => {
|
|
setMarker(e.latlng);
|
|
reverseGeocode(e.latlng.lat, e.latlng.lng);
|
|
});
|
|
|
|
const lat = parseFloat(latInput.value);
|
|
const lng = parseFloat(lngInput.value);
|
|
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
|
|
setMarker({ lat, lng });
|
|
map.setView({ lat, lng }, 14);
|
|
}
|
|
}
|
|
|
|
btnMap?.addEventListener('click', () => {
|
|
mapWrapper.hidden = false;
|
|
ensureLeaflet(() => {
|
|
setTimeout(() => initMap(), 50);
|
|
});
|
|
});
|
|
|
|
btnMapSearch?.addEventListener('click', () => {
|
|
const q = (mapSearch?.value || '').trim();
|
|
if (!q) return;
|
|
geocodeAndPlace(q);
|
|
});
|
|
|
|
btnEventAddressSearch?.addEventListener('click', () => {
|
|
const parts = [
|
|
streetInput?.value || '',
|
|
zipInput?.value || '',
|
|
cityInput?.value || '',
|
|
regionInput?.value || '',
|
|
].map(v => v.trim()).filter(Boolean);
|
|
if (!parts.length) {
|
|
alert('Bitte zuerst mindestens einen Teil der Adresse eingeben.');
|
|
return;
|
|
}
|
|
if (eventAddressStatus) eventAddressStatus.textContent = 'Suche passende Adressen ...';
|
|
geocodeAddressCandidates(parts.join(', '), (results) => {
|
|
const first = results?.[0];
|
|
if (!first) return;
|
|
const normalized = normalizeGeoResult(first);
|
|
if (streetInput && normalized.street) streetInput.value = normalized.street;
|
|
if (zipInput && normalized.zip) zipInput.value = normalized.zip;
|
|
if (cityInput && normalized.city) cityInput.value = normalized.city;
|
|
if (regionInput) regionInput.value = normalized.region;
|
|
if (latInput) latInput.value = Number.isNaN(normalized.lat) ? '' : normalized.lat.toFixed(7);
|
|
if (lngInput) lngInput.value = Number.isNaN(normalized.lng) ? '' : normalized.lng.toFixed(7);
|
|
if (eventAddressStatus) eventAddressStatus.textContent = `Gewählte Adresse: ${normalized.label || [normalized.street, `${normalized.zip} ${normalized.city}`.trim(), normalized.region].filter(Boolean).join(', ')}`;
|
|
if (!mapWrapper.hidden) {
|
|
ensureLeaflet(() => {
|
|
initMap();
|
|
if (!Number.isNaN(normalized.lat) && !Number.isNaN(normalized.lng)) {
|
|
setMarker({ lat: normalized.lat, lng: normalized.lng });
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
btnEventBrowserLocation?.addEventListener('click', () => {
|
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
|
alert('Für Browser-Standort und Adressübernahme bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
|
window.PKTConsent?.openPreferences?.();
|
|
return;
|
|
}
|
|
if (!navigator.geolocation) {
|
|
alert('Dein Browser unterstützt keine Standortabfrage.');
|
|
return;
|
|
}
|
|
if (eventAddressStatus) eventAddressStatus.textContent = 'Browser-Standort wird abgerufen ...';
|
|
navigator.geolocation.getCurrentPosition((position) => {
|
|
reverseGeocodeProfileAddress(position.coords.latitude, position.coords.longitude, (data) => {
|
|
const normalized = normalizeGeoResult(data);
|
|
if (streetInput && normalized.street) streetInput.value = normalized.street;
|
|
if (zipInput && normalized.zip) zipInput.value = normalized.zip;
|
|
if (cityInput && normalized.city) cityInput.value = normalized.city;
|
|
if (regionInput) regionInput.value = normalized.region;
|
|
if (latInput) latInput.value = Number.isNaN(normalized.lat) ? '' : normalized.lat.toFixed(7);
|
|
if (lngInput) lngInput.value = Number.isNaN(normalized.lng) ? '' : normalized.lng.toFixed(7);
|
|
if (eventAddressStatus) eventAddressStatus.textContent = `Browser-Adresse übernommen: ${normalized.label || [normalized.street, `${normalized.zip} ${normalized.city}`.trim(), normalized.region].filter(Boolean).join(', ')}`;
|
|
ensureLeaflet(() => {
|
|
mapWrapper.hidden = false;
|
|
initMap();
|
|
if (!Number.isNaN(normalized.lat) && !Number.isNaN(normalized.lng)) {
|
|
setMarker({ lat: normalized.lat, lng: normalized.lng });
|
|
}
|
|
});
|
|
});
|
|
}, () => {
|
|
if (eventAddressStatus) eventAddressStatus.textContent = '';
|
|
alert('Der Browser-Standort konnte nicht abgerufen werden.');
|
|
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 });
|
|
});
|
|
|
|
btnEventMapCurrentLocation?.addEventListener('click', () => {
|
|
btnEventBrowserLocation?.click();
|
|
});
|
|
|
|
function syncMapFromAddressFields() {
|
|
const parts = [
|
|
streetInput?.value || '',
|
|
zipInput?.value || '',
|
|
cityInput?.value || '',
|
|
regionInput?.value || '',
|
|
].map((value) => value.trim()).filter(Boolean);
|
|
if (!parts.length) return;
|
|
const query = parts.join(', ');
|
|
if (query === lastAutoGeocodeQuery) return;
|
|
lastAutoGeocodeQuery = query;
|
|
geocodeAddressCandidates(query, (results) => {
|
|
const first = results?.[0];
|
|
if (!first) return;
|
|
const normalized = normalizeGeoResult(first);
|
|
if (latInput) latInput.value = Number.isNaN(normalized.lat) ? '' : normalized.lat.toFixed(7);
|
|
if (lngInput) lngInput.value = Number.isNaN(normalized.lng) ? '' : normalized.lng.toFixed(7);
|
|
if (!mapWrapper.hidden) {
|
|
ensureLeaflet(() => {
|
|
initMap();
|
|
if (!Number.isNaN(normalized.lat) && !Number.isNaN(normalized.lng)) {
|
|
setMarker({ lat: normalized.lat, lng: normalized.lng });
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
[streetInput, zipInput, cityInput, regionInput].forEach((field) => {
|
|
field?.addEventListener('change', syncMapFromAddressFields);
|
|
field?.addEventListener('blur', syncMapFromAddressFields);
|
|
});
|
|
|
|
(function(){
|
|
const setProfileStatus = (message = '') => {
|
|
if (!profileAddressStatus) return;
|
|
profileAddressStatus.textContent = message;
|
|
};
|
|
|
|
const renderSelectedAddress = (label) => {
|
|
if (!profileAddressSelectedLabel) return;
|
|
if (!label) {
|
|
profileAddressSelectedLabel.textContent = '';
|
|
profileAddressSelectedLabel.style.display = 'none';
|
|
return;
|
|
}
|
|
profileAddressSelectedLabel.textContent = `Gewählte Adresse: ${label}`;
|
|
profileAddressSelectedLabel.style.display = '';
|
|
};
|
|
|
|
const setNeedsValidation = (needsValidation) => {
|
|
if (profileAddressNeedsValidation) {
|
|
profileAddressNeedsValidation.style.display = needsValidation ? '' : 'none';
|
|
}
|
|
};
|
|
|
|
const normalizeResult = (result) => {
|
|
return normalizeGeoResult(result);
|
|
};
|
|
|
|
const applyProfileAddress = (result) => {
|
|
const normalized = normalizeResult(result);
|
|
|
|
if (profileStreetInput && normalized.street) profileStreetInput.value = normalized.street;
|
|
if (profileZipInput && normalized.zip) profileZipInput.value = normalized.zip;
|
|
if (profileCityInput && normalized.city) profileCityInput.value = normalized.city;
|
|
if (profileRegionViewInput) profileRegionViewInput.value = normalized.region;
|
|
if (profileRegionInput) profileRegionInput.value = normalized.region;
|
|
if (profileLatInput) profileLatInput.value = Number.isNaN(normalized.lat) ? '' : normalized.lat.toFixed(7);
|
|
if (profileLngInput) profileLngInput.value = Number.isNaN(normalized.lng) ? '' : normalized.lng.toFixed(7);
|
|
|
|
renderSelectedAddress(normalized.label || [normalized.street, `${normalized.zip} ${normalized.city}`.trim(), normalized.region].filter(Boolean).join(', '));
|
|
setNeedsValidation(false);
|
|
setProfileStatus('Adresse übernommen.');
|
|
};
|
|
|
|
const openProfileResultsModal = (results) => {
|
|
if (!profileAddressResults || !profileAddressResultsModal) return;
|
|
profileAddressResults.innerHTML = '';
|
|
setProfileStatus('Bitte wähle den passenden Adress-Treffer aus.');
|
|
|
|
results.forEach((result) => {
|
|
const normalized = normalizeResult(result);
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'btn ghost';
|
|
button.style.width = '100%';
|
|
button.style.textAlign = 'left';
|
|
button.style.justifyContent = 'flex-start';
|
|
button.innerHTML = `<strong>${normalized.street || normalized.city || 'Treffer'}</strong><br><span class="muted small">${normalized.label || ''}</span>`;
|
|
button.addEventListener('click', () => {
|
|
applyProfileAddress(result);
|
|
profileAddressResultsModal.classList.remove('open');
|
|
});
|
|
profileAddressResults.appendChild(button);
|
|
});
|
|
|
|
profileAddressResultsModal.classList.add('open');
|
|
};
|
|
|
|
const syncProfileRegionView = () => {
|
|
if (profileRegionViewInput && profileRegionInput) {
|
|
profileRegionInput.value = profileRegionViewInput.value.trim();
|
|
}
|
|
};
|
|
|
|
const clearProfileCoordinates = () => {
|
|
if (profileLatInput) profileLatInput.value = '';
|
|
if (profileLngInput) profileLngInput.value = '';
|
|
renderSelectedAddress('');
|
|
setNeedsValidation(true);
|
|
setProfileStatus('');
|
|
};
|
|
|
|
[profileStreetInput, profileZipInput, profileCityInput].forEach((input) => {
|
|
input?.addEventListener('input', clearProfileCoordinates);
|
|
});
|
|
|
|
profileRegionViewInput?.addEventListener('input', () => {
|
|
syncProfileRegionView();
|
|
clearProfileCoordinates();
|
|
});
|
|
|
|
profileLocateButton?.addEventListener('click', () => {
|
|
const parts = [
|
|
profileStreetInput?.value || '',
|
|
profileZipInput?.value || '',
|
|
profileCityInput?.value || '',
|
|
profileRegionViewInput?.value || '',
|
|
].map(v => v.trim()).filter(Boolean);
|
|
|
|
if (!parts.length) {
|
|
alert('Bitte zuerst mindestens einen Teil der Adresse eingeben.');
|
|
return;
|
|
}
|
|
|
|
setProfileStatus('Suche passende Adressen ...');
|
|
geocodeAddressCandidates(parts.join(', '), (results) => openProfileResultsModal(results));
|
|
});
|
|
|
|
profileBrowserLocationButton?.addEventListener('click', () => {
|
|
if (!window.PKTConsent || !window.PKTConsent.has('external_services')) {
|
|
alert('Für die Browser-Adresse bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.');
|
|
window.PKTConsent?.openPreferences?.();
|
|
return;
|
|
}
|
|
if (!navigator.geolocation) {
|
|
alert('Dieser Browser unterstützt keine Geolokalisierung.');
|
|
return;
|
|
}
|
|
|
|
setProfileStatus('Browser-Standort wird abgefragt ...');
|
|
navigator.geolocation.getCurrentPosition((position) => {
|
|
const lat = position.coords.latitude;
|
|
const lng = position.coords.longitude;
|
|
reverseGeocodeProfileAddress(lat, lng, (result) => {
|
|
applyProfileAddress(result);
|
|
setProfileStatus('Adresse aus dem Browser-Standort übernommen.');
|
|
});
|
|
}, () => {
|
|
setProfileStatus('');
|
|
alert('Die Browser-Position konnte nicht gelesen werden.');
|
|
}, {
|
|
enableHighAccuracy: true,
|
|
timeout: 10000,
|
|
maximumAge: 300000,
|
|
});
|
|
});
|
|
|
|
syncProfileRegionView();
|
|
setNeedsValidation(false);
|
|
})();
|
|
|
|
<?php if ($editing): ?>
|
|
(function(){
|
|
const modal = document.getElementById('modalEvent');
|
|
if (modal) {
|
|
modal.classList.add('open');
|
|
}
|
|
})();
|
|
<?php endif; ?>
|
|
|
|
<?php if ($editingChild): ?>
|
|
(function(){
|
|
const modal = document.getElementById('modalChild');
|
|
if (modal) {
|
|
modal.classList.add('open');
|
|
}
|
|
})();
|
|
<?php endif; ?>
|
|
|
|
(function(){
|
|
const birthInput = document.getElementById('cBirth');
|
|
const ageInput = document.getElementById('cAge');
|
|
|
|
if (!birthInput || !ageInput) return;
|
|
|
|
const calculateAge = (value) => {
|
|
if (!value) return null;
|
|
const birthDate = new Date(`${value}T00:00:00`);
|
|
if (Number.isNaN(birthDate.getTime())) return null;
|
|
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
if (birthDate > today) return null;
|
|
|
|
let age = today.getFullYear() - birthDate.getFullYear();
|
|
const monthDiff = today.getMonth() - birthDate.getMonth();
|
|
const dayDiff = today.getDate() - birthDate.getDate();
|
|
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
|
|
age -= 1;
|
|
}
|
|
return Math.max(0, age);
|
|
};
|
|
|
|
const syncAgeField = () => {
|
|
const calculatedAge = calculateAge(birthInput.value);
|
|
if (calculatedAge === null) {
|
|
ageInput.readOnly = false;
|
|
ageInput.removeAttribute('aria-readonly');
|
|
return;
|
|
}
|
|
|
|
ageInput.value = String(calculatedAge);
|
|
ageInput.readOnly = true;
|
|
ageInput.setAttribute('aria-readonly', 'true');
|
|
};
|
|
|
|
birthInput.addEventListener('input', syncAgeField);
|
|
syncAgeField();
|
|
})();
|
|
|
|
(function(){
|
|
const statusText = document.getElementById('locationBrowserStatusText');
|
|
const hintText = document.getElementById('locationBrowserStatusHint');
|
|
const profilePreference = document.body?.dataset.locationPreference || 'prompt';
|
|
|
|
if (!statusText || !hintText) return;
|
|
|
|
const profileLabels = {
|
|
disabled: 'Deaktiviert',
|
|
prompt: 'Beim nächsten Mal fragen',
|
|
enabled: 'Immer verwenden',
|
|
};
|
|
|
|
const render = (browserState) => {
|
|
if (browserState === 'granted') {
|
|
statusText.textContent = 'Der Browser erlaubt Standortzugriffe derzeit.';
|
|
} else if (browserState === 'denied') {
|
|
statusText.textContent = 'Der Browser blockiert Standortzugriffe derzeit.';
|
|
} else if (browserState === 'prompt') {
|
|
statusText.textContent = 'Der Browser fragt bei Standortzugriffen erneut nach.';
|
|
} else {
|
|
statusText.textContent = 'Der Browserstatus konnte nicht eindeutig ermittelt werden.';
|
|
}
|
|
|
|
const profileState = profileLabels[profilePreference] || profileLabels.prompt;
|
|
hintText.textContent = `Website-Einstellung: ${profileState}. Wenn Browser und Website-Einstellung sich unterscheiden, gilt technisch zuerst die Browserfreigabe.`;
|
|
};
|
|
|
|
if (!('permissions' in navigator) || typeof navigator.permissions.query !== 'function') {
|
|
render('unknown');
|
|
return;
|
|
}
|
|
|
|
navigator.permissions.query({ name: 'geolocation' }).then((result) => {
|
|
render(result.state);
|
|
if (typeof result.onchange !== 'undefined') {
|
|
result.onchange = () => render(result.state);
|
|
}
|
|
}).catch(() => render('unknown'));
|
|
})();
|
|
</script>
|