This commit is contained in:
@@ -78,6 +78,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
||||
- In der Admin-Freigabe werden aktivierte Ortsanbieter serverseitig parallel durchsucht: OpenStreetMap/Nominatim, Google Places und Azure Maps. Ein Admin wählt einen Treffer gezielt aus; je Ort können Verknüpfungen mehrerer Anbieter gespeichert werden. Google-Gesamtwertung und Bewertungsanzahl werden erst nach der ausgewählten Google-Verknüpfung geladen.
|
||||
- Die Admin-Ansicht für Orte und Veranstaltungen hat die Unterpunkte `Freigaben`, `Bestehende Ortsbearbeitung` mit Suchfeld und `Fehlende Angaben`. Letzterer filtert veröffentlichte Einträge nach Verknüpfungen, die bei mindestens einem aktuell aktivierten Anbieter fehlen.
|
||||
- Nach dem Übernehmen eines externen Treffers wird dessen temporäre Trefferliste entfernt. Der Bearbeiten-Button ist in jeder dieser Unteransichten zur Prüfung von Name und Adresse verfügbar.
|
||||
- Berechtigte Admins können veröffentlichte Orte und Veranstaltungen direkt korrigieren. Wenn die vollständige OSM-Suchanfrage keinen Treffer liefert, werden zusätzlich vereinfachte Adress- und Name-Ort-Abfragen versucht.
|
||||
- Eine manuelle Admin-Suche überträgt Name und Adressdaten des offenen Eintrags an die aktivierten Ortsanbieter; Datenschutz- und Consent-Hinweise müssen vor dem produktiven Aktivieren geprüft werden.
|
||||
|
||||
## Vorgemerkte To-dos
|
||||
|
||||
@@ -34,6 +34,7 @@ Stand: 2026-08-10
|
||||
- Direktbearbeitung noch nicht veröffentlichter Orts- und Veranstaltungsvorschläge sowie Rechteauflösung für deren Freigabe: `src/App/CommunityAccess.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`
|
||||
- Server-seitige Ortsanbieter-Suche und sichere Trefferauswahl für die Admin-Freigabe: `src/App/PlaceProviderLookup.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`
|
||||
- Admin-Listen für bestehende Ortsbearbeitung und fehlende Anbieter-Verknüpfungen: `src/App/ListingCatalog.php`, `src/App/AccountPages.php`, `partials/landing/account/dashboard.php`
|
||||
- Direkte Admin-Korrekturen veröffentlichter Orts- und Veranstaltungsdaten: `src/App/ListingCatalog.php`, `src/App/AccountPages.php`, `partials/landing/account/dashboard.php`
|
||||
- Rollenvergabe per Benutzersuche im User Management: `src/App/AccountPages.php`, `partials/landing/account/dashboard.php`, `src/App/CommunityAccess.php`
|
||||
- Legacy-Eigen-Events wurden erweitert in `src/App/AccountPages.php` und `Internal/db/schema.sql` um Kategorie- und Bildfelder
|
||||
- Kategorien-Prüfung und Zusammenführung für Seiten-Admins liegen ebenfalls in `partials/landing/account/dashboard.php` mit Logik in `src/App/ListingCatalog.php`
|
||||
|
||||
@@ -78,6 +78,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
|
||||
- In admin approvals, enabled place providers are searched server-side in parallel: OpenStreetMap/Nominatim, Google Places, and Azure Maps. An admin deliberately selects a match; links to multiple providers can be saved for one place. Google aggregate rating and rating count are loaded only after the selected Google link.
|
||||
- The admin view for places and events has the sub-items `Approvals`, `Existing place editing` with a search field, and `Missing information`. The latter filters published entries by links missing from at least one currently enabled provider.
|
||||
- After an external match is accepted, its temporary result list is removed. The Edit button is available in each of these subviews to check the name and address.
|
||||
- Authorized admins can correct published places and events directly. If the full OSM query returns no match, simplified address and name-city queries are tried as well.
|
||||
- A manual admin lookup sends the open entry's name and address data to enabled place providers; privacy and consent notices must be reviewed before enabling this in production.
|
||||
|
||||
## Deferred To-dos
|
||||
|
||||
@@ -34,6 +34,7 @@ Updated: 2026-08-10
|
||||
- direct editing for not-yet-published place and event-style suggestions plus approval-right resolution: `src/App/CommunityAccess.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`
|
||||
- server-side place-provider lookup and secure match selection for admin approvals: `src/App/PlaceProviderLookup.php`, `src/App/AccountPages.php`, `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`
|
||||
- admin lists for existing place editing and missing provider links: `src/App/ListingCatalog.php`, `src/App/AccountPages.php`, `partials/landing/account/dashboard.php`
|
||||
- direct admin corrections of published place and event data: `src/App/ListingCatalog.php`, `src/App/AccountPages.php`, `partials/landing/account/dashboard.php`
|
||||
- role assignment via user search in User Management: `src/App/AccountPages.php`, `partials/landing/account/dashboard.php`, `src/App/CommunityAccess.php`
|
||||
- legacy own events were extended in `src/App/AccountPages.php` and `Internal/db/schema.sql` with category and image fields
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf:
|
||||
- In der Admin-Freigabe können berechtigte Nutzer offene Orte und Veranstaltungen serverseitig bei aktivierten Anbietern (OpenStreetMap, Google Places, Azure Maps) suchen und einen Treffer gezielt verknüpfen; Google-Gesamtwertung und Bewertungsanzahl werden erst nach einer bewussten Google-Verknüpfung geladen.
|
||||
- Die Admin-Ansicht für Orte und Veranstaltungen gliedert sich in `Freigaben`, `Bestehende Ortsbearbeitung` mit Suchfeld und `Fehlende Angaben`; Letztere listet veröffentlichte Einträge ohne Verknüpfung zu mindestens einem aktuell aktivierten Anbieter.
|
||||
- Nach dem Übernehmen eines externen Treffers wird dessen temporäre Trefferliste entfernt. Der Button `Bearbeiten` steht in allen drei Admin-Unteransichten zur Prüfung von Name und Adresse bereit.
|
||||
- Berechtigte Admins speichern Korrekturen an veröffentlichten Orten und Veranstaltungen direkt; für eine erfolglose OSM-Suche werden zusätzlich vereinfachte Adress- und Name-Ort-Abfragen versucht.
|
||||
- Für später vorgemerkt: direkte Kalender-Anbindung großer Anbieter wie Google und Microsoft/Outlook per OAuth, zusätzlich zum bestehenden ICS-Feed
|
||||
- Interne Projektdateien und das Datenbankschema liegen nicht mehr im Root, sondern unter `Internal/`
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ $restoreEventDraftActive = !empty($restoreEventDraftActive);
|
||||
$editingEvent = isset($editEvent) && $editEvent !== null;
|
||||
$editingListing = isset($editListing) && $editListing !== null;
|
||||
$editing = $editingEvent || $editingListing;
|
||||
$editingSuggestionDirectly = $editingListing && (string)($editListing['status'] ?? '') !== 'published';
|
||||
$editingSuggestionDirectly = $editingListing && ((string)($editListing['status'] ?? '') !== 'published' || !empty($canReviewListings));
|
||||
$eventDraftValues = (!$editing && (($section ?? '') === 'events') && $restoreEventDraftActive && is_array($restoredEventDraft ?? null))
|
||||
? $restoredEventDraft
|
||||
: [];
|
||||
@@ -1744,7 +1744,7 @@ if ($isAdminArea && !empty($canManageSystemSettings)) {
|
||||
<p class="muted small" style="margin:0;">Die Änderung wird erst nach Prüfung durch einen Admin freigegeben.</p>
|
||||
</div>
|
||||
<?php elseif ($editingSuggestionDirectly): ?>
|
||||
<p class="muted small" style="margin:0;">Dieser Vorschlag ist noch nicht veröffentlicht und kann direkt aktualisiert werden.</p>
|
||||
<p class="muted small" style="margin:0;"><?= !empty($canReviewListings) ? 'Dieser Eintrag kann mit deinen Admin-Rechten direkt aktualisiert werden.' : 'Dieser Vorschlag ist noch nicht veröffentlicht und kann direkt aktualisiert werden.' ?></p>
|
||||
<?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; ?>
|
||||
|
||||
@@ -503,18 +503,20 @@ final class AccountPages
|
||||
trim((string)($payload['description'] ?? '')),
|
||||
trim((string)($payload['title'] ?? ''))
|
||||
);
|
||||
$canDirectListingEdit = is_array($existingListing)
|
||||
&& (string)($existingListing['status'] ?? '') !== 'published'
|
||||
&& (
|
||||
(int)($existingListing['created_by'] ?? 0) === $userId
|
||||
|| $canReviewListings
|
||||
$canDirectListingEdit = is_array($existingListing) && (
|
||||
$canReviewListings
|
||||
|| (
|
||||
(string)($existingListing['status'] ?? '') !== 'published'
|
||||
&& (int)($existingListing['created_by'] ?? 0) === $userId
|
||||
)
|
||||
);
|
||||
if ($action === 'event_update') {
|
||||
if ($canDirectListingEdit) {
|
||||
$listingCatalog->updateEditableSuggestion(
|
||||
(int)($_POST['listing_id'] ?? 0),
|
||||
$payload
|
||||
);
|
||||
if ($canReviewListings) {
|
||||
$listingCatalog->updateAdminEntry((int)($_POST['listing_id'] ?? 0), $payload);
|
||||
} else {
|
||||
$listingCatalog->updateEditableSuggestion((int)($_POST['listing_id'] ?? 0), $payload);
|
||||
}
|
||||
$info = $entryKind === 'place' ? 'Ort aktualisiert.' : 'Veranstaltung aktualisiert.';
|
||||
} else {
|
||||
$listingCatalog->submitUpdateRequest(
|
||||
|
||||
@@ -922,6 +922,24 @@ final class ListingCatalog
|
||||
$this->saveDashboardEntry((int)($entry['created_by'] ?? 0), $data, $listingId, $normalizedListingStatus, $normalizedPlaceStatus);
|
||||
}
|
||||
|
||||
public function updateAdminEntry(int $listingId, array $data): void
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$entry = $this->getMemberEntry($listingId);
|
||||
if (!$entry || !in_array((string)($entry['status'] ?? ''), ['draft', 'published'], true)) {
|
||||
throw new \RuntimeException('Eintrag nicht gefunden.');
|
||||
}
|
||||
$this->assertNoDuplicateEntry($data, $listingId);
|
||||
$this->saveDashboardEntry(
|
||||
(int)($entry['created_by'] ?? 0),
|
||||
$data,
|
||||
$listingId,
|
||||
(string)($entry['status'] ?? 'published'),
|
||||
(string)($entry['place_status'] ?? 'published'),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public function listOpenModerationRequests(): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
@@ -1122,7 +1140,7 @@ final class ListingCatalog
|
||||
}
|
||||
}
|
||||
|
||||
public function saveDashboardEntry(int $userId, array $data, ?int $listingId = null, string $listingStatus = 'published', string $placeStatus = 'published'): int
|
||||
public function saveDashboardEntry(int $userId, array $data, ?int $listingId = null, string $listingStatus = 'published', string $placeStatus = 'published', bool $allowForeignListing = false): int
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$entryType = (string)($data['entry_kind'] ?? 'editorial_event');
|
||||
@@ -1185,7 +1203,7 @@ final class ListingCatalog
|
||||
$categoryId = isset($category['id']) ? (int)$category['id'] : null;
|
||||
|
||||
if ($listingId !== null && $listingId > 0) {
|
||||
$existing = $this->getDashboardEntry($userId, $listingId);
|
||||
$existing = $allowForeignListing ? $this->getMemberEntry($listingId) : $this->getDashboardEntry($userId, $listingId);
|
||||
if (!$existing) {
|
||||
throw new \RuntimeException('Eintrag nicht gefunden.');
|
||||
}
|
||||
@@ -1216,15 +1234,15 @@ final class ListingCatalog
|
||||
'id' => $placeId,
|
||||
]);
|
||||
}
|
||||
$listingStmt = $this->pdo->prepare(
|
||||
$listingSql =
|
||||
'UPDATE listings
|
||||
SET listing_type = :listingType, title = :title, teaser_public = :teaser, description = :description,
|
||||
image_path = :imagePath, special_conditions_note = :specialConditionsNote, visibility = :visibility,
|
||||
supports_registration = 0, supports_capacity = :supportsCapacity, supports_pricing = :supportsPricing,
|
||||
is_recurring = :isRecurring, status = :listingStatus, updated_at = NOW()
|
||||
WHERE id = :id AND created_by = :uid'
|
||||
);
|
||||
$listingStmt->execute([
|
||||
WHERE id = :id' . ($allowForeignListing ? '' : ' AND created_by = :uid');
|
||||
$listingStmt = $this->pdo->prepare($listingSql);
|
||||
$listingParams = [
|
||||
'listingType' => $entryType,
|
||||
'title' => $title,
|
||||
'teaser' => $teaser !== '' ? $teaser : $title,
|
||||
@@ -1237,8 +1255,11 @@ final class ListingCatalog
|
||||
'isRecurring' => $entryType === 'editorial_event' && $recurrenceMode !== 'single' ? 1 : 0,
|
||||
'listingStatus' => $listingStatus,
|
||||
'id' => $listingId,
|
||||
'uid' => $userId,
|
||||
]);
|
||||
];
|
||||
if (!$allowForeignListing) {
|
||||
$listingParams['uid'] = $userId;
|
||||
}
|
||||
$listingStmt->execute($listingParams);
|
||||
|
||||
$this->pdo->prepare('DELETE FROM listing_occurrences WHERE listing_id = :listingId')->execute(['listingId' => $listingId]);
|
||||
$this->saveOccurrenceAndPrices($listingId, $entryType, $startsAt, $recurrenceMode, $weekdayValues, $recurrenceUntil, $capacityTotal, $data);
|
||||
|
||||
@@ -88,17 +88,18 @@ final class PlaceProviderLookup
|
||||
|
||||
private function searchOsm(string $query, array $entry): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($this->osmQueries($query, $entry) as $osmQuery) {
|
||||
$url = 'https://nominatim.openstreetmap.org/search?' . http_build_query([
|
||||
'format' => 'jsonv2',
|
||||
'addressdetails' => 1,
|
||||
'limit' => 3,
|
||||
'q' => $query,
|
||||
'q' => $osmQuery,
|
||||
]);
|
||||
$rows = $this->cachedOsmRequest($url);
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
continue;
|
||||
}
|
||||
$results = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row) || empty($row['osm_type']) || empty($row['osm_id'])) {
|
||||
continue;
|
||||
@@ -109,9 +110,28 @@ final class PlaceProviderLookup
|
||||
$id = (string)$row['osm_id'];
|
||||
$results[] = $this->result('osm', $type . ':' . $id, (string)($row['name'] ?? $row['display_name'] ?? ''), (string)($row['display_name'] ?? ''), $lat, $lng, $entry, 'https://www.openstreetmap.org/' . $type . '/' . rawurlencode($id));
|
||||
}
|
||||
if ($results !== []) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function osmQueries(string $primaryQuery, array $entry): array
|
||||
{
|
||||
$addressQuery = trim(implode(', ', array_filter([
|
||||
(string)($entry['street'] ?? ''),
|
||||
trim((string)($entry['zip'] ?? '') . ' ' . (string)($entry['city'] ?? '')),
|
||||
'Deutschland',
|
||||
])));
|
||||
$nameCityQuery = trim(implode(', ', array_filter([
|
||||
(string)($entry['title'] ?? $entry['place_title'] ?? ''),
|
||||
(string)($entry['city'] ?? ''),
|
||||
'Deutschland',
|
||||
])));
|
||||
return array_values(array_unique(array_filter([$primaryQuery, $addressQuery, $nameCityQuery])));
|
||||
}
|
||||
|
||||
private function searchGoogle(string $query, array $entry): array
|
||||
{
|
||||
$payload = ['textQuery' => $query, 'languageCode' => 'de', 'regionCode' => 'DE'];
|
||||
|
||||
Reference in New Issue
Block a user