Files
papa-kind-treff.info/partials/landing/account/dashboard.php
Lars Gebhardt-Kusche 5401e89a2b
All checks were successful
Deploy / deploy (push) Successful in 53s
ydasd
2026-08-19 20:33:08 +02:00

2714 lines
154 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);
$requestedCreateListingKind = isset($_GET['create_listing']) && in_array((string)$_GET['create_listing'], ['place', 'editorial_event'], true)
? (string)$_GET['create_listing']
: null;
$restoreEventDraftActive = !empty($restoreEventDraftActive);
$editingEvent = isset($editEvent) && $editEvent !== null;
$editingListing = isset($editListing) && $editListing !== null;
$editing = $editingEvent || $editingListing;
$eventDraftValues = (!$editing && (($section ?? '') === 'events') && $restoreEventDraftActive && is_array($restoredEventDraft ?? null))
? $restoredEventDraft
: [];
$currentEntryKind = $editingListing
? (string)($editListing['listing_type'] ?? 'editorial_event')
: ((($section ?? 'events') === 'places') ? ($requestedCreateListingKind ?? 'place') : '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']));
} elseif (!empty($eventDraftValues['starts_at'])) {
$startVal = (string)$eventDraftValues['starts_at'];
}
$withChildrenValue = $editingEvent
? (((int)($editEvent['allow_kids'] ?? 1)) === 1 ? 'yes' : 'no')
: (!empty($eventDraftValues['with_children']) ? (string)$eventDraftValues['with_children'] : (($editingListing && $currentEntryKind === 'editorial_event' && !empty($editListing['supports_registration'])) ? 'yes' : 'yes'));
$eventTitle = $editingEvent ? 'Event bearbeiten' : ($editingListing ? ($currentEntryKind === 'place' ? 'Änderung für Ort anfragen' : 'Änderung für Veranstaltung anfragen') : ((($section ?? 'events') === 'places') ? 'Ort oder Veranstaltung anlegen' : 'Neues Event anlegen'));
$submitLabel = $editingEvent ? 'Event speichern' : ($editingListing ? 'Änderungsanfrage senden' : ((($section ?? 'events') === 'places') ? 'Eintrag zur Freigabe einreichen' : 'Event anlegen'));
$titleValue = $editingEvent ? (string)($editEvent['title'] ?? '') : (!empty($eventDraftValues['title']) ? (string)$eventDraftValues['title'] : (string)($editListing['title'] ?? ''));
$teaserValue = $editingEvent ? (string)($editEvent['teaser_public'] ?? '') : (string)($editListing['teaser_public'] ?? '');
$descriptionValue = $editingEvent ? (string)($editEvent['description'] ?? '') : (!empty($eventDraftValues['description']) ? (string)$eventDraftValues['description'] : (string)($editListing['description'] ?? ''));
$streetValue = $editingEvent ? (string)($editEvent['street'] ?? '') : (isset($eventDraftValues['street']) ? (string)$eventDraftValues['street'] : (string)($editListing['street'] ?? ''));
$zipValue = $editingEvent ? (string)($editEvent['zip'] ?? '') : (isset($eventDraftValues['zip']) ? (string)$eventDraftValues['zip'] : (string)($editListing['zip'] ?? ''));
$cityValue = $editingEvent ? (string)($editEvent['city'] ?? '') : (isset($eventDraftValues['city']) ? (string)$eventDraftValues['city'] : (string)($editListing['city'] ?? ''));
$regionValue = $editingEvent ? (string)($editEvent['region'] ?? '') : (isset($eventDraftValues['region']) ? (string)$eventDraftValues['region'] : (string)($editListing['region'] ?? ''));
$latValue = $editingEvent ? (string)($editEvent['lat'] ?? '') : (isset($eventDraftValues['lat']) ? (string)$eventDraftValues['lat'] : (string)($editListing['lat'] ?? ''));
$lngValue = $editingEvent ? (string)($editEvent['lng'] ?? '') : (isset($eventDraftValues['lng']) ? (string)$eventDraftValues['lng'] : (string)($editListing['lng'] ?? ''));
$visibilityValue = $editingEvent ? (string)($editEvent['visibility'] ?? 'public') : (!empty($eventDraftValues['visibility']) ? (string)$eventDraftValues['visibility'] : (string)($editListing['visibility'] ?? 'public'));
$categorySlugValue = $editingEvent ? (string)($editEvent['category_slug'] ?? '') : (!empty($eventDraftValues['category_slug']) ? (string)$eventDraftValues['category_slug'] : (string)($editListing['category_slug'] ?? ''));
$imagePathValue = $editingEvent ? (string)($editEvent['image_path'] ?? '') : (string)($editListing['image_path'] ?? '');
$capacityValue = $editingEvent ? (string)($editEvent['max_participants'] ?? '') : (isset($eventDraftValues['max_participants']) ? (string)$eventDraftValues['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 = !empty($eventDraftValues['category_input'])
? (string)$eventDraftValues['category_input']
: ($categoryTitleMap[$categorySlugValue] ?? $categorySlugValue);
$eventLocationModeValue = $editingEvent ? (string)($editEvent['location_source_type'] ?? 'custom') : (!empty($eventDraftValues['event_location_mode']) ? (string)$eventDraftValues['event_location_mode'] : 'custom');
$eventLocationSourceIdValue = $editingEvent && !empty($editEvent['location_source_listing_id']) ? (int)$editEvent['location_source_listing_id'] : (!empty($eventDraftValues['event_location_source_id']) ? (int)$eventDraftValues['event_location_source_id'] : 0);
$eventLocationPlaceOptions = [];
$eventLocationEditorialOptions = [];
foreach (($eventLocationOptions ?? []) as $locationOption) {
if ((string)($locationOption['listing_type'] ?? '') === 'place') {
$eventLocationPlaceOptions[] = $locationOption;
} elseif ((string)($locationOption['listing_type'] ?? '') === 'editorial_event') {
$eventLocationEditorialOptions[] = $locationOption;
}
if ((int)($locationOption['id'] ?? 0) !== $eventLocationSourceIdValue) {
continue;
}
$optionTypeLabel = (string)($locationOption['listing_type'] ?? '') === 'place' ? 'Ort' : 'Veranstaltung';
$optionAddressLabel = trim(implode(', ', array_filter([
(string)($locationOption['street'] ?? ''),
trim((string)($locationOption['zip'] ?? '') . ' ' . (string)($locationOption['city'] ?? '')),
])));
if ($eventLocationModeValue !== 'custom' && !empty($locationOption['category_title'])) {
$categoryInputValue = (string)$locationOption['category_title'];
$categorySlugValue = (string)($locationOption['category_slug'] ?? $categorySlugValue);
$streetValue = (string)($locationOption['street'] ?? $streetValue);
$zipValue = (string)($locationOption['zip'] ?? $zipValue);
$cityValue = (string)($locationOption['city'] ?? $cityValue);
$regionValue = (string)($locationOption['region'] ?? $regionValue);
$latValue = (string)($locationOption['lat'] ?? $latValue);
$lngValue = (string)($locationOption['lng'] ?? $lngValue);
}
break;
}
$usesDatabaseEventLocation = $currentEntryKind === 'event' && $eventLocationModeValue !== 'custom';
$eventAddressInfoValue = trim(implode(', ', array_filter([
$streetValue,
trim($zipValue . ' ' . $cityValue),
$regionValue,
])));
$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',
'places' => 'Orte & Veranstaltungen',
'community' => 'Community',
'settings' => 'Einstellungen',
];
if (!empty($canManageProfileLevels)) {
$sectionLinks['profile-levels'] = 'Profil-Levels';
}
$showEventTypeChoice = !$editing && (($section ?? 'events') === 'places') && $requestedCreateListingKind === null;
$modalSectionTarget = ($editingListing || ($section ?? '') === 'places') ? 'places' : 'events';
if (!empty($canManageCategories)) {
$sectionLinks['categories'] = 'Kategorien';
}
if (!empty($canManageSystemSettings)) {
$sectionLinks['system'] = 'System';
}
?>
<main class="section section--dashboard">
<div class="container" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;">
<div>
<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">
<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 === 'profile-levels' && !empty($canManageProfileLevels)): ?>
<?php
$systemRoleAssignmentsByRole = [];
foreach (($systemRoleAssignments ?? []) as $assignment) {
$roleKey = (string)($assignment['role'] ?? '');
if ($roleKey === '') {
continue;
}
$systemRoleAssignmentsByRole[$roleKey][] = $assignment;
}
?>
<section class="account-panel">
<div class="account-panel__head">
<h2>Profil-Levels</h2>
</div>
<div class="account-panel__body">
<div class="card" style="margin-bottom:18px;">
<strong>Kürzlich erreicht</strong>
<?php if (!empty($recentHighLevelUsers) && !empty($recentHighLevelThresholdLabel)): ?>
<ul class="dash-list" style="margin-top:12px;">
<?php foreach ($recentHighLevelUsers as $recentHighLevelUser): ?>
<?php
$recentHighLevelUserName = (string)($recentHighLevelUser['display_name'] ?: trim(((string)($recentHighLevelUser['first_name'] ?? '')) . ' ' . ((string)($recentHighLevelUser['last_name'] ?? ''))) ?: 'Mitglied');
$recentHighLevelUserLevel = trim(((string)(($recentHighLevelUser['level']['icon'] ?? ''))) . ' ' . ((string)(($recentHighLevelUser['level']['label'] ?? ''))));
?>
<li>
<strong><?= htmlspecialchars($recentHighLevelUserName, ENT_QUOTES) ?></strong>
<div class="muted small" style="margin-top:4px;">
<?= htmlspecialchars($recentHighLevelUserLevel, ENT_QUOTES) ?> ·
<?= number_format((float)($recentHighLevelUser['points'] ?? 0), 1, ',', '.') ?> Punkte ·
<?= htmlspecialchars(date('d.m.Y', strtotime((string)($recentHighLevelUser['reached_at'] ?? ''))), ENT_QUOTES) ?>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p class="muted small" style="margin-top:12px;">Keine neuen Aufstiege vorhanden.</p>
<?php endif; ?>
</div>
<div class="card" style="margin-bottom:18px;">
<strong>Benutzer suchen</strong>
<form method="get" class="stack gap-12" style="margin-top:14px;">
<input type="hidden" name="section" value="profile-levels">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="profileLevelUserQuery">Name oder E-Mail</label>
<input id="profileLevelUserQuery" name="profile_level_user_query" class="input" value="<?= htmlspecialchars((string)($profileLevelUserSearchQuery ?? ''), ENT_QUOTES) ?>">
</div>
<div class="stack gap-6" style="justify-content:end;">
<span class="label" aria-hidden="true">&nbsp;</span>
<button class="btn" type="submit">Suchen</button>
</div>
</div>
</form>
<?php if (!empty($profileLevelUserSearchResults)): ?>
<div class="stack gap-12" style="margin-top:18px;">
<?php foreach ($profileLevelUserSearchResults as $searchedUser): ?>
<div class="card" style="padding:14px;">
<div class="flex between center-y" style="gap:12px; flex-wrap:wrap;">
<div>
<strong><?= htmlspecialchars((string)($searchedUser['display_name'] ?: trim(((string)($searchedUser['first_name'] ?? '')) . ' ' . ((string)($searchedUser['last_name'] ?? ''))) ?: 'Mitglied'), ENT_QUOTES) ?></strong>
<div class="muted small" style="margin-top:6px;">
ID: <?= (int)($searchedUser['id'] ?? 0) ?> ·
<?= htmlspecialchars((string)($searchedUser['email'] ?? ''), ENT_QUOTES) ?> ·
Punkte: <?= number_format((float)($searchedUser['community_points'] ?? 0), 1, ',', '.') ?> ·
Level: <?= htmlspecialchars(trim(((string)(($searchedUser['community_level']['icon'] ?? ''))) . ' ' . ((string)(($searchedUser['community_level']['label'] ?? '')))), ENT_QUOTES) ?>
</div>
<?php if (!empty($searchedUser['roles'])): ?>
<div class="muted small" style="margin-top:6px;">System-Level: <?= htmlspecialchars(implode(', ', array_map('strval', (array)$searchedUser['roles'])), ENT_QUOTES) ?></div>
<?php endif; ?>
</div>
</div>
<form method="post" class="stack gap-12" style="margin-top:14px;">
<input type="hidden" name="action" value="community_points_adjust">
<input type="hidden" name="target_user_id" value="<?= (int)($searchedUser['id'] ?? 0) ?>">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="communityPointsAmount<?= (int)($searchedUser['id'] ?? 0) ?>">Punkte erhöhen</label>
<input id="communityPointsAmount<?= (int)($searchedUser['id'] ?? 0) ?>" name="points_amount" class="input" type="number" min="0.1" step="0.1" required>
</div>
<div class="stack gap-6">
<label class="label" for="communityPointsReason<?= (int)($searchedUser['id'] ?? 0) ?>">Begründung</label>
<input id="communityPointsReason<?= (int)($searchedUser['id'] ?? 0) ?>" name="points_reason" class="input" required>
</div>
</div>
<div class="flex gap-12" style="flex-wrap:wrap;">
<button class="btn" type="submit">Punkte vergeben</button>
</div>
</form>
</div>
<?php endforeach; ?>
</div>
<?php elseif (($profileLevelUserSearchQuery ?? '') !== ''): ?>
<p class="muted small" style="margin-top:14px;">Keine passenden Benutzer gefunden.</p>
<?php endif; ?>
</div>
<div class="card">
<strong>Community-Levels</strong>
<div class="stack gap-12" style="margin-top:14px;">
<?php foreach (($communityLevelDefinitions ?? []) as $levelDefinition): ?>
<details class="card" style="padding:14px;">
<summary style="cursor:pointer; font-weight:700;">
<?= htmlspecialchars((string)($levelDefinition['label'] ?? ''), ENT_QUOTES) ?> · ab <?= number_format((float)($levelDefinition['min'] ?? 0), 1, ',', '.') ?> Punkten
</summary>
<form method="post" class="stack gap-12" style="margin-top:14px;">
<input type="hidden" name="action" value="community_level_save">
<input type="hidden" name="level_id" value="<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="communityLevelLabel<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">Name</label>
<input id="communityLevelLabel<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>" name="label" class="input" value="<?= htmlspecialchars((string)($levelDefinition['label'] ?? ''), ENT_QUOTES) ?>" required>
</div>
<div class="stack gap-6">
<label class="label" for="communityLevelMin<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">Mindestpunkte</label>
<input id="communityLevelMin<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>" name="min_points" class="input" type="number" min="0" step="0.1" value="<?= htmlspecialchars((string)($levelDefinition['min'] ?? '0'), ENT_QUOTES) ?>" required>
</div>
</div>
<div class="stack gap-6">
<label class="label" for="communityLevelIcon<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">Icon (optional)</label>
<input id="communityLevelIcon<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>" name="icon" class="input" value="<?= htmlspecialchars((string)($levelDefinition['icon'] ?? ''), ENT_QUOTES) ?>">
</div>
<div class="stack gap-8">
<span class="label">Rechte</span>
<div class="chips" style="flex-wrap:wrap;">
<?php foreach (($communityLevelRightDefinitions ?? []) as $rightKey => $rightDefinition): ?>
<label class="chip" style="cursor:pointer;">
<input type="hidden" name="rights[<?= htmlspecialchars((string)$rightKey, ENT_QUOTES) ?>]" value="0">
<input type="checkbox" name="rights[<?= htmlspecialchars((string)$rightKey, ENT_QUOTES) ?>]" value="1" <?= !empty($levelDefinition['rights'][$rightKey]) ? 'checked' : '' ?> style="margin-right:6px;">
<?= htmlspecialchars((string)($rightDefinition['label'] ?? $rightKey), ENT_QUOTES) ?>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="flex gap-12" style="flex-wrap:wrap;">
<button class="btn" type="submit">Speichern</button>
</div>
</form>
<form method="post" onsubmit="return confirm('Community-Level wirklich löschen?');">
<input type="hidden" name="action" value="community_level_delete">
<input type="hidden" name="level_id" value="<?= htmlspecialchars((string)($levelDefinition['id'] ?? ''), ENT_QUOTES) ?>">
<button class="btn ghost" type="submit">Löschen</button>
</form>
</details>
<?php endforeach; ?>
</div>
</div>
<div class="card" style="margin-top:18px;">
<strong>Neues Community-Level</strong>
<form method="post" class="stack gap-12" style="margin-top:14px;">
<input type="hidden" name="action" value="community_level_save">
<input type="hidden" name="level_id" value="">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="communityLevelLabelNew">Name</label>
<input id="communityLevelLabelNew" name="label" class="input" required>
</div>
<div class="stack gap-6">
<label class="label" for="communityLevelMinNew">Mindestpunkte</label>
<input id="communityLevelMinNew" name="min_points" class="input" type="number" min="0" step="0.1" value="0" required>
</div>
</div>
<div class="stack gap-6">
<label class="label" for="communityLevelIconNew">Icon (optional)</label>
<input id="communityLevelIconNew" name="icon" class="input">
</div>
<div class="stack gap-8">
<span class="label">Rechte</span>
<div class="chips" style="flex-wrap:wrap;">
<?php foreach (($communityLevelRightDefinitions ?? []) as $rightKey => $rightDefinition): ?>
<label class="chip" style="cursor:pointer;">
<input type="hidden" name="rights[<?= htmlspecialchars((string)$rightKey, ENT_QUOTES) ?>]" value="0">
<input type="checkbox" name="rights[<?= htmlspecialchars((string)$rightKey, ENT_QUOTES) ?>]" value="1" style="margin-right:6px;">
<?= htmlspecialchars((string)($rightDefinition['label'] ?? $rightKey), ENT_QUOTES) ?>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="flex gap-12">
<button class="btn" type="submit">Community-Level anlegen</button>
</div>
</form>
</div>
<div class="card" style="margin-top:18px;">
<strong>System-Level</strong>
<div class="stack gap-12" style="margin-top:14px;">
<?php foreach (($systemLevelDefinitions ?? []) as $systemLevel): ?>
<div class="card" style="padding:14px;">
<strong><?= htmlspecialchars((string)($systemLevel['label'] ?? ''), ENT_QUOTES) ?></strong>
<?php if (!empty($systemRoleAssignmentsByRole[(string)($systemLevel['key'] ?? '')])): ?>
<div class="muted small" style="margin-top:8px;">
Aktuell:
<?php
$assignmentLabels = [];
foreach ($systemRoleAssignmentsByRole[(string)$systemLevel['key']] as $assignment) {
$assignmentLabels[] = (string)($assignment['display_name'] ?? 'Mitglied');
}
echo htmlspecialchars(implode(', ', $assignmentLabels), ENT_QUOTES);
?>
</div>
<?php endif; ?>
<ul class="dash-list" style="margin-top:12px;">
<?php foreach (($systemLevel['rights'] ?? []) as $systemRight): ?>
<li><?= htmlspecialchars((string)$systemRight, ENT_QUOTES) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'events'): ?>
<section class="account-panel" id="events">
<div class="account-panel__head">
<h2>Deine Events</h2>
<p class="muted">Hier verwaltest du nur echte eigene Events und Events, an denen du teilnimmst.</p>
</div>
<div class="account-panel__body">
<div class="account-inline-tabs-wrap">
<div class="account-inline-tabs" role="tablist" aria-label="Event-Bereiche">
<button class="account-inline-tabs__button is-active" type="button" role="tab" aria-selected="true" data-event-tab-trigger="my-events">Meine Events</button>
<button class="account-inline-tabs__button" type="button" data-modal-open="modalEvent">Neues Event anlegen</button>
<button class="account-inline-tabs__button" type="button" role="tab" aria-selected="false" data-event-tab-trigger="sync">Kalendersynchronisation</button>
<button class="account-inline-tabs__button" type="button" role="tab" aria-selected="false" data-event-tab-trigger="past-events">Abgelaufene Events</button>
</div>
</div>
<div class="account-inline-tab-panel" data-event-tab-panel="my-events">
<div class="card" style="margin-bottom:18px;">
<strong>Eigene Events</strong>
</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; ?>
<div class="card" style="margin:22px 0 14px;">
<div style="display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap:wrap;">
<div>
<strong>Angemeldete Events</strong>
<p class="muted small" style="margin:8px 0 0;">Events anderer Nutzer, bei denen du eingetragen bist.</p>
</div>
<div class="flex gap-12" style="flex-wrap:wrap;">
<a class="btn ghost" href="/search">Event Suche</a>
</div>
</div>
</div>
<?php if (!$eventsJoinedUpcoming): ?>
<p class="muted small">Aktuell nimmst du an keinen zukünftigen Events teil.</p>
<?php else: ?>
<ul class="dash-list" style="margin-top:10px;">
<?php foreach ($eventsJoinedUpcoming 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>
<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,
!empty($entry['host_name']) ? 'Von ' . (string)$entry['host_name'] : null,
!empty($entry['participation_status']) ? 'Status: ' . (string)$entry['participation_status'] : null,
]))), ENT_QUOTES) ?>
</div>
</div>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<div class="account-inline-tab-panel" data-event-tab-panel="sync" hidden>
<div class="card">
<strong>Kalender-Synchronisierung</strong>
<p class="muted small" style="margin:8px 0 0;">Exportiert werden immer alle eigenen Events und alle Events, an denen du teilnimmst.</p>
<?php if (!empty($calendarExportUrl)): ?>
<div class="flex gap-12" style="margin-top:14px; flex-wrap:wrap;">
<a class="btn" href="<?= htmlspecialchars((string)$calendarExportUrl, ENT_QUOTES) ?>">ICS herunterladen</a>
</div>
<?php endif; ?>
</div>
<div class="card" style="margin-top:18px;">
<strong>Kalender abonnieren</strong>
<p class="muted small" style="margin:8px 0 0;">Der persönliche Kalender-Feed ist für Apps gedacht, die Internet-Kalender abonnieren können. So bleiben Änderungen, Absagen oder Löschungen bei der nächsten Synchronisierung aktuell.</p>
<div class="stack gap-6" style="margin-top:14px;">
<label class="label" for="calendarFeedUrl">Persönlicher Kalender-Feed</label>
<input id="calendarFeedUrl" class="input" type="text" readonly value="<?= htmlspecialchars((string)$calendarFeedUrl, ENT_QUOTES) ?>">
</div>
<div class="stack gap-6" style="margin-top:14px;">
<strong style="font-size:15px;">So nutzt du den Feed</strong>
<p class="muted small" style="margin:0;">1. Feed-Link kopieren.</p>
<p class="muted small" style="margin:0;">2. Im Kalender-Anbieter `Kalender abonnieren` oder `Per URL hinzufügen` wählen.</p>
<p class="muted small" style="margin:0;">3. Den Link einfügen und speichern.</p>
</div>
</div>
<div class="card" style="margin-top:18px;">
<strong>Wichtiger Unterschied</strong>
<p class="muted small" style="margin:8px 0 0;">„ICS herunterladen“ ist nur eine Momentaufnahme für den manuellen Import. Spätere Änderungen in Papa-Kind-Treff werden dadurch nicht automatisch in deinem privaten Kalender nachgezogen.</p>
<p class="muted small" style="margin:8px 0 0;">Für laufende Aktualisierungen solltest du deshalb den persönlichen Kalender-Feed abonnieren.</p>
</div>
</div>
<div class="account-inline-tab-panel" data-event-tab-panel="past-events" hidden>
<div class="card" style="margin-bottom:18px;">
<strong>Vergangene eigene Events</strong>
<p class="muted small" style="margin:8px 0 0;">Hier siehst du deine bereits abgelaufenen oder vergangenen eigenen Events.</p>
</div>
<?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; ?>
<div class="card" style="margin:22px 0 14px;">
<strong>Vergangene Teilnahmen</strong>
<p class="muted small" style="margin:8px 0 0;">Events anderer Nutzer, an denen du in der Vergangenheit teilgenommen hast.</p>
</div>
<?php if (!$eventsJoinedPast): ?>
<p class="muted small">Keine vergangenen Event-Teilnahmen.</p>
<?php else: ?>
<ul class="dash-list" style="margin-top:10px;">
<?php foreach ($eventsJoinedPast as $entry): ?>
<li>
<div>
<strong><?= htmlspecialchars((string)$entry['title'], ENT_QUOTES) ?></strong>
<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,
!empty($entry['host_name']) ? 'Von ' . (string)$entry['host_name'] : null,
!empty($entry['participation_status']) ? 'Status: ' . (string)$entry['participation_status'] : null,
]))), ENT_QUOTES) ?>
</div>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'places'): ?>
<section class="account-panel" id="places">
<div class="account-panel__head">
<h2>Orte &amp; Veranstaltungen</h2>
<p class="muted">Hier legst du dauerhafte Orte wie Cafés oder Spielplätze sowie zeitlich begrenzte Veranstaltungen 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">Ort oder Veranstaltung anlegen</button>
</div>
<div class="card" style="margin-bottom:18px;">
<strong>Deine eingereichten Vorschläge</strong>
<?php
$pendingListings = array_values(array_filter($userSubmittedListings ?? [], static function (array $entry): bool {
return (string)($entry['status'] ?? '') !== 'published';
}));
?>
<?php if (!$pendingListings): ?>
<p class="muted small" style="margin:8px 0 0;">Aktuell warten keine eigenen Orte oder Veranstaltungen auf Prüfung.</p>
<?php else: ?>
<ul class="dash-list" style="margin-top:12px;">
<?php foreach ($pendingListings as $entry): ?>
<li>
<strong><?= htmlspecialchars((string)$entry['title'], ENT_QUOTES) ?></strong>
<span class="badge"><?= ($entry['listing_type'] ?? '') === 'place' ? 'Ort' : 'Veranstaltung' ?></span>
<span class="badge" style="background:#fff7e6; color:#8a5a00;">
<?= (string)($entry['moderation_request_status'] ?? '') === 'rejected' ? 'Abgelehnt' : 'Wartet auf Freigabe' ?>
</span>
<?php if (!empty($entry['moderation_review_note'])): ?>
<div class="muted small" style="margin-top:6px;">Admin-Hinweis: <?= htmlspecialchars((string)$entry['moderation_review_note'], ENT_QUOTES) ?></div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php if (!$directoryListings): ?>
<p class="muted small">Noch keine freigegebenen Orte oder Veranstaltungen vorhanden.</p>
<?php else: ?>
<div class="card" style="margin-bottom:18px;">
<strong>Freigegebene Orte &amp; Veranstaltungen</strong>
<p class="muted small" style="margin:8px 0 0;">Hier siehst du die systemweit verfügbaren Einträge. Für Änderungen oder Löschungen ist immer eine begründete Anfrage nötig.</p>
</div>
<ul class="dash-list" style="margin-top:10px;">
<?php foreach ($directoryListings as $entry): ?>
<?php $openRequestsForEntry = $listingRequestMap[(int)$entry['id']] ?? []; ?>
<li>
<div style="display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap: wrap;">
<div>
<strong><?= htmlspecialchars((string)$entry['title'], ENT_QUOTES) ?></strong>
<span class="badge"><?= ($entry['listing_type'] ?? '') === 'place' ? 'Ort' : 'Veranstaltung' ?></span>
<?php if (!empty($entry['category_title'])): ?>
<span class="badge"><?= htmlspecialchars((string)$entry['category_title'], ENT_QUOTES) ?></span>
<?php endif; ?>
<?php if (in_array('update', $openRequestsForEntry, true)): ?>
<span class="badge" style="background:#fff7e6; color:#8a5a00;">Änderung angefragt</span>
<?php endif; ?>
<?php if (in_array('delete', $openRequestsForEntry, true)): ?>
<span class="badge" style="background:#fee2e2; color:#991b1b;">Löschung angefragt</span>
<?php endif; ?>
<div class="muted small" style="margin-top:4px;">
<?= htmlspecialchars(trim(implode(' · ', array_filter([
trim(implode(', ', array_filter([
(string)($entry['street'] ?? ''),
trim((string)($entry['zip'] ?? '') . ' ' . (string)($entry['city'] ?? '')),
]))),
(string)($entry['region'] ?? ''),
!empty($entry['starts_at']) ? (string)$entry['starts_at'] : null,
]))), ENT_QUOTES) ?>
</div>
</div>
<div class="stack gap-8" style="min-width:min(100%, 320px);">
<div class="flex gap-8" style="flex-wrap: wrap;">
<?php if (!in_array('update', $openRequestsForEntry, true)): ?>
<a class="btn ghost" href="/dashboard?section=places&edit_listing=<?= (int)$entry['id'] ?>#places">Änderung anfragen</a>
<?php endif; ?>
</div>
<?php if (!in_array('delete', $openRequestsForEntry, true)): ?>
<form method="post" action="/dashboard?section=places#places" class="stack gap-6">
<input type="hidden" name="action" value="listing_delete_request">
<input type="hidden" name="listing_id" value="<?= (int)$entry['id'] ?>">
<label class="label" for="deleteReason<?= (int)$entry['id'] ?>">Löschanfrage begründen</label>
<textarea id="deleteReason<?= (int)$entry['id'] ?>" name="moderation_reason" class="textarea" rows="2" placeholder="Warum sollte dieser Eintrag entfernt werden?" required></textarea>
<button class="btn ghost" type="submit">Löschung anfragen</button>
</form>
<?php endif; ?>
</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>
</section>
<?php endif; ?>
<?php if ($section === 'categories' && !empty($canManageCategories)): ?>
<section class="account-panel">
<div class="account-panel__head">
<h2>Kategorien</h2>
</div>
<div class="account-panel__body">
<datalist id="categoryMergeSuggestions">
<?php foreach ($listingCategories as $targetCategory): ?>
<option value="<?= htmlspecialchars((string)$targetCategory['title'], ENT_QUOTES) ?>"></option>
<?php endforeach; ?>
</datalist>
<div class="card" style="margin-bottom:18px;">
<form method="get" class="stack gap-12">
<input type="hidden" name="section" value="categories">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="categoryQuery">Kategorie suchen</label>
<input id="categoryQuery" name="category_query" class="input" value="<?= htmlspecialchars((string)($categorySearchQuery ?? ''), ENT_QUOTES) ?>" list="categoryMergeSuggestions">
</div>
<div class="stack gap-6" style="justify-content:end;">
<span class="label" aria-hidden="true">&nbsp;</span>
<button class="btn" type="submit">Suchen</button>
</div>
</div>
</form>
</div>
<?php if (($categorySearchQuery ?? '') !== ''): ?>
<div class="card" style="margin-bottom:18px;">
<strong>Suchergebnisse</strong>
<?php if (!empty($categorySearchResults)): ?>
<div class="stack gap-10" style="margin-top:12px;">
<?php foreach ($categorySearchResults as $categoryItem): ?>
<div class="card" style="padding:12px;">
<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>
<form method="post" class="stack gap-8" style="margin-top:12px;">
<input type="hidden" name="action" value="category_merge">
<input type="hidden" name="current_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
<div class="stack gap-6">
<label class="label" for="targetCategorySearchInput<?= (int)($categoryItem['id'] ?? 0) ?>">Zweite Kategorie</label>
<input
id="targetCategorySearchInput<?= (int)($categoryItem['id'] ?? 0) ?>"
name="other_category_input"
class="input"
list="categoryMergeSuggestions"
placeholder="Kategorie suchen"
required
>
</div>
<div class="stack gap-6">
<label class="label" for="keepCategorySearchChoice<?= (int)($categoryItem['id'] ?? 0) ?>">Behalten</label>
<select id="keepCategorySearchChoice<?= (int)($categoryItem['id'] ?? 0) ?>" name="keep_category_choice" class="select">
<option value="current"><?= htmlspecialchars((string)$categoryItem['title'], ENT_QUOTES) ?></option>
<option value="other">Ausgewählte zweite Kategorie</option>
</select>
</div>
<div class="flex gap-8" style="flex-wrap:wrap;">
<button class="btn ghost" type="submit">Zusammenführen</button>
</div>
</form>
<?php if (!empty($categoryItem['needs_review'])): ?>
<form method="post" style="margin-top:12px;">
<input type="hidden" name="action" value="category_confirm">
<input type="hidden" name="source_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
<button class="btn" type="submit">Bestätigen</button>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="muted small" style="margin-top:12px;">Keine passenden Kategorien gefunden.</p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($categoryReviewItems)): ?>
<div class="stack gap-10">
<?php foreach ($categoryReviewItems as $categoryItem): ?>
<div class="card" style="padding:12px;">
<div>
<strong><?= htmlspecialchars((string)$categoryItem['title'], ENT_QUOTES) ?></strong>
<span class="badge" style="margin-left:8px;">Neu</span>
<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>
<form method="post" class="stack gap-8" style="margin-top:12px;">
<input type="hidden" name="action" value="category_merge">
<input type="hidden" name="current_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
<div class="stack gap-6">
<label class="label" for="targetCategoryInput<?= (int)($categoryItem['id'] ?? 0) ?>">Zweite Kategorie</label>
<input
id="targetCategoryInput<?= (int)($categoryItem['id'] ?? 0) ?>"
name="other_category_input"
class="input"
list="categoryMergeSuggestions"
placeholder="Kategorie suchen"
required
>
</div>
<div class="stack gap-6">
<label class="label" for="keepCategoryChoice<?= (int)($categoryItem['id'] ?? 0) ?>">Behalten</label>
<select id="keepCategoryChoice<?= (int)($categoryItem['id'] ?? 0) ?>" name="keep_category_choice" class="select">
<option value="current"><?= htmlspecialchars((string)$categoryItem['title'], ENT_QUOTES) ?></option>
<option value="other">Ausgewählte zweite Kategorie</option>
</select>
</div>
<div class="flex gap-8" style="flex-wrap:wrap;">
<button class="btn ghost" type="submit">Zusammenführen</button>
</div>
</form>
<form method="post" style="margin-top:12px;">
<input type="hidden" name="action" value="category_confirm">
<input type="hidden" name="source_category_slug" value="<?= htmlspecialchars((string)$categoryItem['slug'], ENT_QUOTES) ?>">
<button class="btn" type="submit">Bestätigen</button>
</form>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="muted small">Aktuell gibt es keine neuen Kategorien.</p>
<?php endif; ?>
</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 id="eventEditorForm" class="stack gap-12" style="margin-top: 10px;" method="post" action="/dashboard?section=<?= htmlspecialchars($modalSectionTarget, ENT_QUOTES) ?>#<?= htmlspecialchars($modalSectionTarget, ENT_QUOTES) ?>" 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; ?>
<input type="hidden" name="event_location_source_id" id="eventLocationSourceId" value="<?= $eventLocationSourceIdValue > 0 ? (int)$eventLocationSourceIdValue : '' ?>">
<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 && $showEventTypeChoice): ?>
<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="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;">Ort: dauerhaft verfügbar oder Veranstaltung: zeitlich begrenzter Hinweis.</p>
</div>
<?php elseif ($editing): ?>
<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?"><?= htmlspecialchars($descriptionValue, ENT_QUOTES) ?></textarea>
</div>
<div class="stack gap-6" id="eventLocationModeWrap">
<label class="label" for="eventLocationMode">Location</label>
<select id="eventLocationMode" name="event_location_mode" class="select">
<option value="place" <?= $eventLocationModeValue === 'place' ? 'selected' : '' ?>>Ort aus Datenbank</option>
<option value="editorial_event" <?= $eventLocationModeValue === 'editorial_event' ? 'selected' : '' ?>>Veranstaltung aus Datenbank</option>
<option value="custom" <?= $eventLocationModeValue === 'custom' ? 'selected' : '' ?>>Benutzerdefiniert</option>
</select>
</div>
<div class="stack gap-6" id="eventLocationSourceWrap">
<label class="label" for="eventLocationSourceSelect" id="eventLocationSourceLabel">Ort aus Datenbank</label>
<select id="eventLocationSourceSelect" class="select">
<option value=""><?= $eventLocationModeValue === 'editorial_event' ? 'Keine aktuell verfügbaren Veranstaltungen' : 'Ort aus der Datenbank auswählen' ?></option>
<?php foreach (($eventLocationModeValue === 'editorial_event' ? $eventLocationEditorialOptions : $eventLocationPlaceOptions) as $locationOption): ?>
<?php
$optionAddressLabel = trim(implode(', ', array_filter([
(string)($locationOption['street'] ?? ''),
trim((string)($locationOption['zip'] ?? '') . ' ' . (string)($locationOption['city'] ?? '')),
])));
$optionDateLabel = !empty($locationOption['starts_at']) ? date('d.m.Y', strtotime((string)$locationOption['starts_at'])) : '';
$optionLabel = trim(implode(' · ', array_filter([
(string)($locationOption['title'] ?? ''),
$optionDateLabel,
$optionAddressLabel,
])));
?>
<option
value="<?= (int)($locationOption['id'] ?? 0) ?>"
data-location='<?= htmlspecialchars(json_encode($locationOption, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES) ?>'
<?= (int)($locationOption['id'] ?? 0) === $eventLocationSourceIdValue ? 'selected' : '' ?>
><?= htmlspecialchars($optionLabel, ENT_QUOTES) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if (($section ?? 'events') === 'events' && !$editingListing): ?>
<div class="flex gap-8" style="flex-wrap:wrap;" id="eventCreateListingActionsWrap">
<button class="btn ghost" type="button" data-event-create-listing="place">Neuen Ort anlegen</button>
<button class="btn ghost" type="button" data-event-create-listing="editorial_event">Neue Veranstaltung anlegen</button>
</div>
<?php endif; ?>
<div class="stack gap-6" id="categoryInputWrap"<?= $usesDatabaseEventLocation ? ' hidden' : '' ?>>
<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="stack gap-6" id="eventCategoryInfoWrap"<?= $usesDatabaseEventLocation ? '' : ' hidden' ?>>
<label class="label">Kategorie</label>
<div class="muted small" id="eventCategoryInfoText" style="margin:0;"><?= htmlspecialchars($categoryInputValue !== '' ? $categoryInputValue : 'Keine Kategorie ausgewählt.', ENT_QUOTES) ?></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>
<?php if ($editingListing || ($section ?? '') === 'places'): ?>
<div class="stack gap-6" id="eventValidUntilWrap"<?= $currentEntryKind === 'event' ? ' hidden' : '' ?>>
<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 class="stack gap-6" id="eventRecurrenceWrap"<?= $currentEntryKind === 'event' ? ' hidden' : '' ?>>
<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>
<?php endif; ?>
<?php if ($editingListing): ?>
<div class="stack gap-6">
<label class="label" for="listingModerationReason">Begründung für die Änderungsanfrage</label>
<textarea id="listingModerationReason" name="moderation_reason" class="textarea" rows="3" placeholder="Was stimmt nicht oder was soll angepasst werden?" required></textarea>
<p class="muted small" style="margin:0;">Die Änderung wird erst nach Prüfung durch einen Admin freigegeben.</p>
</div>
<?php elseif (($section ?? '') === 'places'): ?>
<p class="muted small" style="margin:0;">Neue Orte und Veranstaltungen werden erst nach Prüfung durch einen Admin freigegeben.</p>
<?php endif; ?>
<div class="form-grid" id="eventAddressFieldsWrap" style="<?= $usesDatabaseEventLocation ? 'display:none;' : '' ?>">
<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" id="eventAddressStreetWrap" style="<?= $usesDatabaseEventLocation ? 'display:none;' : '' ?>">
<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) ?>">
</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;<?= $usesDatabaseEventLocation ? ' display:none;' : '' ?>" id="eventAddressActionsWrap">
<button class="btn ghost" type="button" id="btnEventAddressSearch">Adresse suchen</button>
<button class="btn ghost" type="button" id="btnEventBrowserLocation">📍 Aktuellen Standort</button>
</div>
<details class="card" id="eventAddressInfoWrap"<?= $usesDatabaseEventLocation ? '' : ' hidden' ?>>
<summary style="cursor:pointer; font-weight:600;">Adresse</summary>
<div class="muted small" id="eventAddressInfoText" style="margin-top:12px;"><?= htmlspecialchars($eventAddressInfoValue !== '' ? $eventAddressInfoValue : 'Kein Ort ausgewählt.', ENT_QUOTES) ?></div>
</details>
<div id="eventAddressStatus" class="muted small" style="margin:0;<?= $usesDatabaseEventLocation ? ' display:none;' : '' ?>"></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) ?>">
<div class="card" id="mapDetails"<?= $usesDatabaseEventLocation ? ' hidden' : '' ?>>
<div class="stack gap-6">
<div class="flex gap-8" style="flex-wrap:wrap; align-items:center;">
<strong>Karte</strong>
<button class="btn ghost" type="button" id="btnEventMapCurrentLocation">📍 Aktuellen Standort auf Karte zeigen</button>
</div>
<div id="mapWrapper" class="map-wrapper">
<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. Änderungen an der Adresse aktualisieren die Karte automatisch.</p>
</div>
<div id="mapContainer" class="map-container"></div>
</div>
</div>
</div>
<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>
<form id="eventDraftRedirectForm" method="post" action="/dashboard?section=events#events" hidden>
<input type="hidden" name="action" value="event_draft_store">
<input type="hidden" name="target_kind" id="eventDraftTargetKind" value="">
</form>
<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');
if (id === 'modalEvent' && window.PKTConsent?.has?.('external_services')) {
ensureLeaflet(() => {
setTimeout(() => initMap(), 80);
});
}
});
});
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 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 mapDetails = document.getElementById('mapDetails');
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 eventLocationModeWrap = document.getElementById('eventLocationModeWrap');
const eventLocationModeSelect = document.getElementById('eventLocationMode');
const eventEditorForm = document.getElementById('eventEditorForm');
const eventDraftRedirectForm = document.getElementById('eventDraftRedirectForm');
const eventDraftTargetKind = document.getElementById('eventDraftTargetKind');
const eventCreateListingButtons = document.querySelectorAll('[data-event-create-listing]');
const eventLocationSourceWrap = document.getElementById('eventLocationSourceWrap');
const eventLocationSourceSelect = document.getElementById('eventLocationSourceSelect');
const eventLocationSourceIdInput = document.getElementById('eventLocationSourceId');
const eventLocationSourceLabel = document.getElementById('eventLocationSourceLabel');
const categoryInputWrap = document.getElementById('categoryInputWrap');
const eventCategoryInfoWrap = document.getElementById('eventCategoryInfoWrap');
const eventCategoryInfoText = document.getElementById('eventCategoryInfoText');
const eventAddressFieldsWrap = document.getElementById('eventAddressFieldsWrap');
const eventAddressStreetWrap = document.getElementById('eventAddressStreetWrap');
const eventAddressActionsWrap = document.getElementById('eventAddressActionsWrap');
const eventAddressInfoWrap = document.getElementById('eventAddressInfoWrap');
const eventAddressInfoText = document.getElementById('eventAddressInfoText');
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 eventDescriptionInput = document.getElementById('evDesc');
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');
const eventTabButtons = document.querySelectorAll('[data-event-tab-trigger]');
const eventTabPanels = document.querySelectorAll('[data-event-tab-panel]');
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;
if (visible) {
element.style.removeProperty('display');
} else {
element.style.display = 'none';
}
element.querySelectorAll('input, select, textarea, button').forEach((field) => {
if (field.type === 'hidden') return;
field.disabled = !visible;
});
}
const allEventLocationOptions = <?= json_encode($eventLocationOptions ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
function buildEventLocationLabel(option) {
if (!option) return '';
const addressLabel = [String(option.street || ''), `${String(option.zip || '')} ${String(option.city || '')}`.trim()]
.filter(Boolean)
.join(', ');
const dateLabel = option.starts_at ? new Date(String(option.starts_at)).toLocaleDateString('de-DE') : '';
return [String(option.title || ''), dateLabel, addressLabel].filter(Boolean).join(' · ');
}
function findEventLocationOptionById(id) {
const normalizedId = Number(id || 0);
if (!normalizedId) return null;
const option = allEventLocationOptions.find((entry) => Number(entry.id || 0) === normalizedId);
if (!option) return null;
return option;
}
function rebuildEventLocationOptions() {
if (!eventLocationSourceSelect) return;
const mode = eventLocationModeSelect?.value || 'custom';
const selectedId = Number(eventLocationSourceIdInput?.value || eventLocationSourceSelect.value || 0);
const filteredOptions = allEventLocationOptions.filter((option) => String(option.listing_type || '') === mode);
const emptyLabel = mode === 'editorial_event'
? 'Keine aktuell verfügbaren Veranstaltungen'
: 'Ort aus der Datenbank auswählen';
eventLocationSourceSelect.innerHTML = '';
const placeholderOption = document.createElement('option');
placeholderOption.value = '';
placeholderOption.textContent = filteredOptions.length === 0 ? emptyLabel : (mode === 'editorial_event' ? 'Veranstaltung aus der Datenbank auswählen' : 'Ort aus der Datenbank auswählen');
eventLocationSourceSelect.appendChild(placeholderOption);
eventLocationSourceSelect.disabled = filteredOptions.length === 0;
filteredOptions.forEach((option) => {
const element = document.createElement('option');
element.value = String(option.id || '');
element.textContent = buildEventLocationLabel(option);
if (Number(option.id || 0) === selectedId) {
element.selected = true;
}
eventLocationSourceSelect.appendChild(element);
});
}
function applyEventLocationOption(option) {
if (!option) return;
if (eventLocationSourceIdInput) eventLocationSourceIdInput.value = String(option.id || '');
if (streetInput) streetInput.value = String(option.street || '');
if (zipInput) zipInput.value = String(option.zip || '');
if (cityInput) cityInput.value = String(option.city || '');
if (regionInput) regionInput.value = String(option.region || '');
if (latInput) latInput.value = option.lat !== null && option.lat !== '' ? Number(option.lat).toFixed(7) : '';
if (lngInput) lngInput.value = option.lng !== null && option.lng !== '' ? Number(option.lng).toFixed(7) : '';
if (categoryInput) categoryInput.value = String(option.category_title || '');
if (categorySlugInput) categorySlugInput.value = String(option.category_slug || '');
if (categoryInputStatus) categoryInputStatus.textContent = '';
if (eventAddressStatus) eventAddressStatus.textContent = '';
}
function syncEventLocationInfo() {
const usesDatabaseLocation = (entryKindInput?.value || 'event') === 'event' && (eventLocationModeSelect?.value || 'custom') !== 'custom';
const addressLabel = [streetInput?.value || '', `${zipInput?.value || ''} ${cityInput?.value || ''}`.trim(), regionInput?.value || '']
.filter(Boolean)
.join(', ');
setSectionState(categoryInputWrap, !usesDatabaseLocation);
if (eventCategoryInfoWrap) {
eventCategoryInfoWrap.hidden = !usesDatabaseLocation;
}
if (eventCategoryInfoText) {
eventCategoryInfoText.textContent = (categoryInput?.value || '').trim() || 'Keine Kategorie ausgewählt.';
}
if (eventAddressInfoWrap) {
eventAddressInfoWrap.hidden = !usesDatabaseLocation;
}
if (eventAddressInfoText) {
eventAddressInfoText.textContent = addressLabel || 'Kein Ort ausgewählt.';
}
if (eventAddressStatus) {
eventAddressStatus.hidden = usesDatabaseLocation;
}
}
function clearEventLocationSelection() {
if (eventLocationSourceIdInput) eventLocationSourceIdInput.value = '';
if (eventLocationSourceSelect) eventLocationSourceSelect.value = '';
if (streetInput) streetInput.value = '';
if (zipInput) zipInput.value = '';
if (cityInput) cityInput.value = '';
if (regionInput) regionInput.value = '';
if (latInput) latInput.value = '';
if (lngInput) lngInput.value = '';
if (categoryInput) categoryInput.value = '';
if (categorySlugInput) categorySlugInput.value = '';
if (categoryInputStatus) categoryInputStatus.textContent = '';
syncEventLocationInfo();
}
function syncEventLocationMode() {
const kind = entryKindInput?.value || 'event';
const mode = eventLocationModeSelect?.value || 'custom';
const isOwnEvent = kind === 'event';
const usesDatabaseLocation = isOwnEvent && mode !== 'custom';
setSectionState(eventLocationModeWrap, isOwnEvent);
setSectionState(eventLocationSourceWrap, usesDatabaseLocation);
setSectionState(eventAddressFieldsWrap, !usesDatabaseLocation);
setSectionState(eventAddressStreetWrap, !usesDatabaseLocation);
setSectionState(eventAddressActionsWrap, !usesDatabaseLocation);
setSectionState(mapDetails, !usesDatabaseLocation);
if (eventLocationSourceLabel) {
eventLocationSourceLabel.textContent = mode === 'editorial_event' ? 'Veranstaltung aus Datenbank' : 'Ort aus Datenbank';
}
rebuildEventLocationOptions();
if (categoryInput) {
if (usesDatabaseLocation) {
categoryInput.removeAttribute('list');
} else {
categoryInput.setAttribute('list', 'listingCategoryList');
}
}
if (usesDatabaseLocation) {
const option = findEventLocationOptionById(eventLocationSourceSelect?.value || eventLocationSourceIdInput?.value || '');
if (option && String(option.listing_type || '') === mode) {
applyEventLocationOption(option);
} else {
clearEventLocationSelection();
}
} else if (isOwnEvent) {
if (eventLocationSourceIdInput) eventLocationSourceIdInput.value = '';
}
syncEventLocationInfo();
}
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 (eventDescriptionInput) eventDescriptionInput.required = kind !== 'event';
if (categoryInput) categoryInput.required = kind !== 'event';
setSectionState(eventChildrenWrap, kind === 'event' || kind === 'editorial_event');
setSectionState(eventCapacityWrap, kind === 'event');
if (eventValidUntilWrap) 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');
if (eventRecurrenceWrap) setSectionState(eventRecurrenceWrap, kind === 'editorial_event');
setSectionState(visibilityDetails, kind === 'event');
if (eventDateLabel) {
eventDateLabel.textContent = kind === 'editorial_event' ? 'Gültig von' : 'Datum';
}
syncEventLocationMode();
}
entryKindButtons.forEach((button) => {
button.addEventListener('click', () => {
syncEventEntryKind(button.getAttribute('data-entry-kind-choice') || 'event');
});
});
eventLocationModeSelect?.addEventListener('change', () => {
clearEventLocationSelection();
syncEventLocationMode();
});
eventLocationSourceSelect?.addEventListener('change', () => {
const mode = eventLocationModeSelect?.value || 'custom';
const option = findEventLocationOptionById(eventLocationSourceSelect.value || '');
if (!option || String(option.listing_type || '') !== mode) {
clearEventLocationSelection();
return;
}
applyEventLocationOption(option);
syncEventLocationInfo();
});
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() {
if (!categorySlugInput || !categoryInputStatus) return;
if ((entryKindInput?.value || 'event') === 'event' && (eventLocationModeSelect?.value || 'custom') !== 'custom') {
categoryInputStatus.textContent = '';
return;
}
const value = (categoryInput?.value || '').trim().toLowerCase();
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 setEventTab(nextTab) {
const activeTab = nextTab || 'my-events';
eventTabButtons.forEach((button) => {
const isActive = button.getAttribute('data-event-tab-trigger') === activeTab;
button.classList.toggle('is-active', isActive);
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
eventTabPanels.forEach((panel) => {
panel.hidden = panel.getAttribute('data-event-tab-panel') !== activeTab;
});
}
function submitEventDraftRedirect(targetKind) {
if (!eventEditorForm || !eventDraftRedirectForm || !eventDraftTargetKind) return;
const allowedFieldNames = new Set([
'title',
'description',
'starts_at',
'with_children',
'max_participants',
'visibility',
'event_location_mode',
'event_location_source_id',
'street',
'zip',
'city',
'region',
'lat',
'lng',
'category_input',
'category_slug',
]);
eventDraftRedirectForm.querySelectorAll('[data-event-draft-copy]').forEach((node) => node.remove());
const formData = new FormData(eventEditorForm);
formData.forEach((value, key) => {
if (!allowedFieldNames.has(key) || typeof value !== 'string') {
return;
}
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = value;
input.setAttribute('data-event-draft-copy', 'true');
eventDraftRedirectForm.appendChild(input);
});
eventDraftTargetKind.value = targetKind;
eventDraftRedirectForm.submit();
}
eventCreateListingButtons.forEach((button) => {
button.addEventListener('click', () => {
submitEventDraftRedirect(button.getAttribute('data-event-create-listing') || 'place');
});
});
eventTabButtons.forEach((button) => {
button.addEventListener('click', () => {
setEventTab(button.getAttribute('data-event-tab-trigger') || 'my-events');
});
});
setEventTab('my-events');
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: '&copy; 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);
}
}
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 || $requestedCreateListingKind !== null || $restoreEventDraftActive): ?>
(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>