@@ -728,6 +745,14 @@ $sectionLinks = [
const zipInput = document.getElementById('evZip');
const cityInput = document.getElementById('evCity');
const regionInput = document.getElementById('evRegion');
+ const profileLocateButton = document.getElementById('btnProfileLocate');
+ 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');
let map, marker;
function ensureLeaflet(callback) {
@@ -818,6 +843,28 @@ $sectionLinks = [
.catch(() => {});
}
+ function geocodeAddressData(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=1&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[0]) {
+ onSuccess?.(data[0]);
+ return;
+ }
+ alert('Adresse konnte nicht eindeutig lokalisiert werden.');
+ })
+ .catch(() => {
+ alert('Adresssuche aktuell nicht erreichbar.');
+ });
+ }
+
function initMap() {
if (map) { map.invalidateSize(); return; }
map = L.map(mapContainer).setView([51.1657, 10.4515], 6);
@@ -863,6 +910,62 @@ $sectionLinks = [
geocodeAndPlace(parts.join(', '));
});
+ (function(){
+ const syncProfileRegionView = () => {
+ if (profileRegionViewInput && profileRegionInput) {
+ profileRegionInput.value = profileRegionViewInput.value.trim();
+ }
+ };
+
+ const clearProfileCoordinates = () => {
+ if (profileLatInput) profileLatInput.value = '';
+ if (profileLngInput) profileLngInput.value = '';
+ };
+
+ [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;
+ }
+
+ geocodeAddressData(parts.join(', '), (result) => {
+ const address = result.address || {};
+ const street = [address.road || '', address.house_number || ''].filter(Boolean).join(' ').trim();
+ const region = address.suburb || address.state || address.county || '';
+ const city = address.city || address.town || address.village || '';
+
+ if (profileStreetInput && street) profileStreetInput.value = street;
+ if (profileZipInput && address.postcode) profileZipInput.value = address.postcode;
+ if (profileCityInput && city) profileCityInput.value = city;
+ if (profileRegionViewInput) profileRegionViewInput.value = region;
+ if (profileRegionInput) profileRegionInput.value = region;
+
+ const lat = parseFloat(result.lat);
+ const lng = parseFloat(result.lon);
+ if (profileLatInput) profileLatInput.value = Number.isNaN(lat) ? '' : lat.toFixed(7);
+ if (profileLngInput) profileLngInput.value = Number.isNaN(lng) ? '' : lng.toFixed(7);
+ });
+ });
+
+ syncProfileRegionView();
+ })();
+
(function(){
const modal = document.getElementById('modalEvent');
diff --git a/public/page/datenschutz.php b/public/page/datenschutz.php
index becb739..d902912 100644
--- a/public/page/datenschutz.php
+++ b/public/page/datenschutz.php
@@ -48,7 +48,7 @@ $clientCookie = $config->cookiePrefix() . 'client';
Wenn du ein Konto anlegst oder den Mitgliederbereich nutzt, verarbeiten wir die dafür notwendigen Daten,
insbesondere E-Mail-Adresse, Passwort-Hash, Verifikationsstatus sowie die von dir gepflegten Profilangaben.
- Dazu können Anzeigename, Name, Ort, Sprachen, Kurzbeschreibung, optionale Kinderangaben,
+ Dazu können Anzeigename, Name, Adresse, Ort, Sprachen, Kurzbeschreibung, optionale Kinderangaben,
eigene Events, Event-Teilnahmen und Community-Inhalte gehören.
@@ -137,6 +137,11 @@ $clientCookie = $config->cookiePrefix() . 'client';
Je nach Auswahl wird dieser nur für den aktuellen Besuch oder darüber hinaus lokal gespeichert,
damit dir passende Treffen in deiner Nähe angezeigt werden können.
+
+ Zusätzlich kannst du im Mitgliederbereich deine Profiladresse über die gleichen externen Adressdienste
+ lokalisieren lassen, die auch für Event-Adressen genutzt werden. Dabei werden die von dir eingegebenen
+ Adressbestandteile an den jeweiligen Geocoding-Dienst übermittelt.
+
Für Karten- und Adressfunktionen werden derzeit externe Dienste genutzt:
diff --git a/schema.sql b/schema.sql
index feb4767..c6576b7 100755
--- a/schema.sql
+++ b/schema.sql
@@ -22,6 +22,7 @@ CREATE TABLE user_profiles (
display_name VARCHAR(120) NOT NULL,
first_name VARBINARY(512) NULL,
last_name VARBINARY(512) NULL,
+ street VARBINARY(512) NULL,
share_level ENUM('basic','papa','papa_contact') NOT NULL DEFAULT 'basic',
children_visibility ENUM('hidden','age_only','details') NOT NULL DEFAULT 'hidden',
zip CHAR(5) NULL,
diff --git a/src/App/AccountPages.php b/src/App/AccountPages.php
index 56c32e6..d2d2c75 100755
--- a/src/App/AccountPages.php
+++ b/src/App/AccountPages.php
@@ -11,6 +11,7 @@ final class AccountPages
private const ENCRYPTED_PROFILE_FIELDS = [
'first_name',
'last_name',
+ 'street',
'contact_phone',
'profession',
'languages',
@@ -185,6 +186,7 @@ final class AccountPages
}
$firstNameEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['first_name']));
$lastNameEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['last_name']));
+ $streetEnc = self::encryptOptionalProfileField($crypto, trim((string)($_POST['street'] ?? '')));
$phoneEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['contact_phone']));
$professionEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['profession']));
$languagesEnc = self::encryptOptionalProfileField($crypto, trim((string)$languages));
@@ -193,13 +195,17 @@ final class AccountPages
if ($profileSettings) {
$profileSettings->ensureSchema();
}
- $stmt = $pdo?->prepare('UPDATE user_profiles SET display_name=:name, first_name=:fname, last_name=:lname, zip=:zip, city=:city, profession=:prof, languages=:langs, about=:about, contact_phone=:phone, location_tracking_preference=:locationPref, updated_at=NOW() WHERE user_id=:id');
+ $stmt = $pdo?->prepare('UPDATE user_profiles SET display_name=:name, first_name=:fname, last_name=:lname, street=:street, zip=:zip, city=:city, region=:region, lat=:lat, lng=:lng, profession=:prof, languages=:langs, about=:about, contact_phone=:phone, location_tracking_preference=:locationPref, updated_at=NOW() WHERE user_id=:id');
$stmt?->execute([
'name' => trim((string)$_POST['display_name']),
'fname' => $firstNameEnc,
'lname' => $lastNameEnc,
+ 'street' => $streetEnc,
'zip' => trim((string)$_POST['zip']),
'city' => trim((string)$_POST['city']),
+ 'region' => trim((string)($_POST['region'] ?? '')),
+ 'lat' => isset($_POST['lat']) && $_POST['lat'] !== '' ? (float)$_POST['lat'] : null,
+ 'lng' => isset($_POST['lng']) && $_POST['lng'] !== '' ? (float)$_POST['lng'] : null,
'prof' => $professionEnc,
'langs' => $languagesEnc,
'about' => $aboutEnc,
@@ -402,6 +408,10 @@ final class AccountPages
'last_name' => '',
'zip' => '',
'city' => '',
+ 'region' => '',
+ 'lat' => null,
+ 'lng' => null,
+ 'street' => '',
'profession' => '',
'languages' => '',
'about' => '',
@@ -427,7 +437,7 @@ final class AccountPages
static fn(string $column): string => 'p.' . $column,
AvatarManager::allProfileColumns()
));
- $stmt = $pdo?->prepare("SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.zip, p.city, p.profession, p.languages, p.about, p.contact_phone, p.location_tracking_preference, $avatarColumns FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1");
+ $stmt = $pdo?->prepare("SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.street, p.zip, p.city, p.region, p.lat, p.lng, p.profession, p.languages, p.about, p.contact_phone, p.location_tracking_preference, $avatarColumns FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1");
$stmt?->execute(['id' => $userId]);
$row = $stmt?->fetch(\PDO::FETCH_ASSOC);
if ($row) {
diff --git a/src/App/ProfileSettings.php b/src/App/ProfileSettings.php
index cfb78e5..3645416 100644
--- a/src/App/ProfileSettings.php
+++ b/src/App/ProfileSettings.php
@@ -61,6 +61,7 @@ final class ProfileSettings
$columnDefinitions = [
'first_name' => 'VARBINARY(512) NULL',
'last_name' => 'VARBINARY(512) NULL',
+ 'street' => 'VARBINARY(512) NULL',
'contact_phone' => 'VARBINARY(512) NULL',
'contact_email' => 'VARBINARY(512) NULL',
'profession' => 'VARBINARY(512) NULL',
@@ -70,6 +71,11 @@ final class ProfileSettings
foreach ($columnDefinitions as $column => $definition) {
if (!$this->hasColumn('user_profiles', $column)) {
+ if ($column === 'street') {
+ $this->pdo->exec('ALTER TABLE user_profiles ADD COLUMN street VARBINARY(512) NULL AFTER last_name');
+ $this->columnCache['user_profiles.street'] = true;
+ $this->columnTypeCache['user_profiles.street'] = 'varbinary';
+ }
continue;
}