diff --git a/Internal/de/PROJECT_CONTEXT.md b/Internal/de/PROJECT_CONTEXT.md index 7eaed4e..8f19d79 100644 --- a/Internal/de/PROJECT_CONTEXT.md +++ b/Internal/de/PROJECT_CONTEXT.md @@ -29,6 +29,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l - Orte und Veranstaltungen sind vorerst in einen separaten Mitgliederbereichspunkt `Orte & Veranstaltungen` verschoben. - In `Orte & Veranstaltungen` können Nutzer dauerhafte Orte und zeitlich begrenzte Veranstaltungen neu anlegen. - Neue Orte und Veranstaltungen bleiben bis zur Freigabe im Status `wartet auf Freigabe` und werden erst danach systemweit veröffentlicht. +- Noch nicht veröffentlichte Orts- und Veranstaltungsvorschläge können vom Ersteller im Mitgliederbereich direkt weiterbearbeitet werden. - Änderungs- und Löschwünsche für veröffentlichte Orte und Veranstaltungen laufen immer über eine begründete Moderationsanfrage. - Vorschläge für neue Orte und Veranstaltungen dürfen schon vor der Freigabe als Ortsvorschlag für Events genutzt werden, solange sie noch offen geprüft werden. - Dubletten bei Orten und Veranstaltungen sollen serverseitig mindestens über Namen und Adresse abgefangen werden. @@ -50,6 +51,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l - In `Profil-Levels` gibt es zusätzlich eine Liste von Nutzern, die in den letzten 30 Tagen mindestens den Rang `Säule der Väter-Community` erreicht haben. - System-Level bleiben getrennt als Rollen `Forum-Admin`, `Site-Admin` und `SiteOwner`. - `Forum-Admin` kann Kategorien bereits bestätigen und zusammenführen sowie Orte und Veranstaltungen freigeben. +- `SiteOwner` und `Site-Admin` erben diese Freigaberechte vollständig mit. - Die Vergabe von System-Leveln läuft im Bereich `Community-Admin` jetzt über eine Benutzersuche statt über rohe Benutzer-IDs; `Site-Admins` bleiben auf die bestehende Rollenlogik begrenzt. - Kategorien liegen nicht mehr unter `System`, sondern in einem eigenen Mitgliederbereichspunkt `Kategorien`; dort werden nur neue Kategorien angezeigt und per Vorschlagsfeld mit bestehenden Kategorien aus der Datenbank zusammengeführt. - Neue Kategorien bleiben dort sichtbar, bis sie von einem Berechtigten bestätigt oder mit einer bestehenden Kategorie zusammengeführt werden. diff --git a/Internal/de/PROJECT_STRUCTURE.md b/Internal/de/PROJECT_STRUCTURE.md index 25e8dec..32f9d23 100644 --- a/Internal/de/PROJECT_STRUCTURE.md +++ b/Internal/de/PROJECT_STRUCTURE.md @@ -28,6 +28,7 @@ Stand: 2026-08-10 - Community-Level-Rechte für Kategorien und Orts-/Veranstaltungsfreigaben: `config/community.php`, `src/App/Community.php`, `src/App/CommunityAccess.php`, `partials/landing/account/community-admin.php` - Preis-, Bild- und Ortseingabe für Orte und Veranstaltungen sowie reduzierte Event-Maske: `partials/landing/account/dashboard.php` plus Speicherung in `src/App/ListingCatalog.php` - Freigaben sowie Änderungs- und Löschanfragen für Orte und Veranstaltungen: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php` +- 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` - Rollenvergabe per Benutzersuche im Community-Admin: `partials/landing/account/community-admin.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` diff --git a/Internal/de/README.md b/Internal/de/README.md index 466c524..b38441d 100644 --- a/Internal/de/README.md +++ b/Internal/de/README.md @@ -29,6 +29,8 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale - Der Zwischenschritt vom eigenen Event zum Anlegen eines neuen Orts oder einer neuen Veranstaltung springt danach wieder in das Event zurück und wählt den neuen Eintrag direkt aus - Orte und sonstige Veranstaltungen wurden vorerst in einen separaten Bereich `Orte & Veranstaltungen` verschoben - Neue Orte und Veranstaltungen müssen vor Veröffentlichung erst durch einen Admin freigegeben werden; Änderungs- und Löschwünsche laufen ebenfalls nur als begründete Anfrage +- Noch nicht veröffentlichte Orts- und Veranstaltungsvorschläge können im Mitgliederbereich direkt weiterbearbeitet werden +- `SiteOwner` übernimmt bei Orts- und Veranstaltungsanfragen vollständig die Freigaberechte von `Forum-Admin` - Die Eingabemasken sind jetzt progressiv aufgebaut: erst Pflichtangaben, optionale Daten in einklappbaren Bereichen - Kategorien laufen jetzt über eine Sucheingabe mit bestehenden Vorschlägen; neue Kategorien werden automatisch angelegt und können im Systembereich von Seiten-Admins zusammengeführt werden - Im Bereich `Events` gibt es jetzt zusätzlich einen ICS-Download und einen persönlichen abonnierbaren Kalender-Feed für alle eigenen Events und Event-Teilnahmen diff --git a/Internal/en/PROJECT_CONTEXT.md b/Internal/en/PROJECT_CONTEXT.md index 4529845..8730a50 100644 --- a/Internal/en/PROJECT_CONTEXT.md +++ b/Internal/en/PROJECT_CONTEXT.md @@ -29,6 +29,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events - Places and event-like entries were moved for now into a separate member-area section `Places & Events`. - Inside `Places & Events`, users can create permanent places and time-limited event-style entries. - New places and event-style entries stay in a pending state until an admin approves them for publication. +- Not-yet-published place and event-style suggestions can be edited directly by their creator inside the member area. - Change and deletion wishes for published places and event-style entries always run through a reasoned moderation request. - Pending new places and event-style entries can already be used as location suggestions for events while they are still under review. - Duplicate submissions for places and event-style entries should be blocked server-side at least by name and address. @@ -50,6 +51,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events - `Profile Levels` also includes a list of users who reached at least the rank `Pillar of the Fathers Community` within the last 30 days. - System levels remain separate as the roles `Forum Admin`, `Site Admin`, and `SiteOwner`. - `Forum Admin` can already confirm and merge categories as well as approve places and event-style entries. +- `SiteOwner` and `Site Admin` fully inherit those approval rights. - System-level assignment in `Community Admin` now runs through user search instead of raw user IDs; site admins remain limited by the existing role rules. - Categories no longer live under `System`, but in their own member-area section `Categories`; that area only shows newly created categories and merges them into existing database categories through a suggestion field. - New categories remain visible there until an authorized user confirms them or merges them into an existing category. diff --git a/Internal/en/PROJECT_STRUCTURE.md b/Internal/en/PROJECT_STRUCTURE.md index 68593e2..ebabef5 100644 --- a/Internal/en/PROJECT_STRUCTURE.md +++ b/Internal/en/PROJECT_STRUCTURE.md @@ -28,6 +28,7 @@ Updated: 2026-08-10 - community-level rights for categories and place/event approvals: `config/community.php`, `src/App/Community.php`, `src/App/CommunityAccess.php`, `partials/landing/account/community-admin.php` - pricing, image upload, and place/address input for places and event-style entries plus a reduced own-event form: `partials/landing/account/dashboard.php` with persistence in `src/App/ListingCatalog.php` - approvals as well as change and deletion requests for places and event-style entries: `src/App/ListingCatalog.php`, `partials/landing/account/dashboard.php`, `partials/landing/account/community-admin.php` +- 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` - role assignment via user search in community admin: `partials/landing/account/community-admin.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 diff --git a/Internal/en/README.md b/Internal/en/README.md index f9d304f..2463cb2 100644 --- a/Internal/en/README.md +++ b/Internal/en/README.md @@ -29,6 +29,8 @@ Papa-Kind-Treff is a PHP-based platform for fathers focused on local events, app - when a user creates a new place or event-style listing from inside an own event, the flow returns to that event afterwards and preselects the newly created entry - places and other event-style entries were moved for now into a separate member-area section `Places & Events` - new places and event-style entries must be approved by an admin before publication; change and deletion wishes also run as reasoned requests +- not-yet-published place and event-style suggestions can be edited directly in the member area +- `SiteOwner` fully inherits the place and event approval rights from `Forum Admin` - the member-area entry forms now follow a progressive approach: required fields first, optional data inside collapsible sections - categories now use a search input with existing suggestions; new categories are created automatically and can be merged by site admins in the system area - the `Events` area now also includes an ICS download and a personal subscribable calendar feed for all own events and event participations diff --git a/README.md b/README.md index a22378c..008e9a4 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf: - Im Mitgliederbereich ist `Events` jetzt wieder auf echte eigene Events und Event-Teilnahmen beschränkt - Orte und sonstige Veranstaltungen laufen jetzt separat unter `Orte & Veranstaltungen` - Neue Orte und Veranstaltungen müssen vor Veröffentlichung erst durch einen Admin freigegeben werden; Änderungs- und Löschwünsche laufen ebenfalls als begründete Anfrage +- Noch nicht veröffentlichte Orts- und Veranstaltungsvorschläge können im Mitgliederbereich direkt weiterbearbeitet werden; veröffentlichte Einträge laufen für normale Mitglieder weiter über begründete Änderungsanfragen +- `SiteOwner` erbt bei Orts- und Veranstaltungsanfragen vollständig die Freigaberechte von `Forum-Admin` - Eigene Events unterstützen aktuell optionalen Beschreibungstext, klare Kinderangabe, optionale Platzzahl, Bild-Upload sowie eine Location aus Datenbank oder benutzerdefinierte Adresse - Bei eigener Event-Location aus der Datenbank werden Adresse und Kategorie nur informativ aus dem gewählten Ort bzw. der gewählten Veranstaltung übernommen und nicht manuell geändert - Aus dem Formular für eigene Events kann direkt in das Anlegen eines neuen Orts oder einer neuen Veranstaltung verzweigt werden diff --git a/partials/landing/account/dashboard.php b/partials/landing/account/dashboard.php index cfaa04a..903ca0c 100755 --- a/partials/landing/account/dashboard.php +++ b/partials/landing/account/dashboard.php @@ -13,6 +13,7 @@ $restoreEventDraftActive = !empty($restoreEventDraftActive); $editingEvent = isset($editEvent) && $editEvent !== null; $editingListing = isset($editListing) && $editListing !== null; $editing = $editingEvent || $editingListing; +$editingSuggestionDirectly = $editingListing && (string)($editListing['status'] ?? '') !== 'published'; $eventDraftValues = (!$editing && (($section ?? '') === 'events') && $restoreEventDraftActive && is_array($restoredEventDraft ?? null)) ? $restoredEventDraft : []; @@ -31,8 +32,8 @@ if ($editingEvent && !empty($editEvent['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')); +$eventTitle = $editingEvent ? 'Event bearbeiten' : ($editingListing ? ($editingSuggestionDirectly ? (($currentEntryKind === 'place') ? 'Ort bearbeiten' : 'Veranstaltung bearbeiten') : (($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 ? ($editingSuggestionDirectly ? 'Änderungen speichern' : 'Ä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'] ?? '')); @@ -751,15 +752,25 @@ if (!empty($canManageSystemSettings)) { @@ -1593,12 +1604,14 @@ if (!empty($canManageSystemSettings)) { - +

Die Änderung wird erst nach Prüfung durch einen Admin freigegeben.

+ +

Dieser Vorschlag ist noch nicht veröffentlicht und kann direkt aktualisiert werden.

Neue Orte und Veranstaltungen werden erst nach Prüfung durch einen Admin freigegeben.

diff --git a/src/App/AccountPages.php b/src/App/AccountPages.php index 4034be7..444e47d 100755 --- a/src/App/AccountPages.php +++ b/src/App/AccountPages.php @@ -182,6 +182,7 @@ final class AccountPages $canManageProfileLevels = $communityAccess ? $communityAccess->canManageRoles($userId) : false; $communityPointsForAccess = $community ? $community->computePoints($userId) : 0.0; $canManageCategories = $communityAccess ? $communityAccess->canManageCategories($userId, $communityPointsForAccess) : false; + $canReviewListings = $communityAccess ? $communityAccess->canReviewListings($userId, $communityPointsForAccess) : false; $allowedSections = ['profile', 'children', 'events', 'places', 'community', 'settings']; if ($canManageSystemSettings) { $allowedSections[] = 'system'; @@ -471,14 +472,28 @@ final class AccountPages trim((string)($payload['description'] ?? '')), trim((string)($payload['title'] ?? '')) ); - if ($action === 'event_update') { - $listingCatalog->submitUpdateRequest( - $userId, - (int)($_POST['listing_id'] ?? 0), - $payload, - (string)($_POST['moderation_reason'] ?? '') + $canDirectListingEdit = is_array($existingListing) + && (string)($existingListing['status'] ?? '') !== 'published' + && ( + (int)($existingListing['created_by'] ?? 0) === $userId + || $canReviewListings ); - $info = $entryKind === 'place' ? 'Änderungsanfrage für den Ort eingereicht.' : 'Änderungsanfrage für die Veranstaltung eingereicht.'; + if ($action === 'event_update') { + if ($canDirectListingEdit) { + $listingCatalog->updateEditableSuggestion( + (int)($_POST['listing_id'] ?? 0), + $payload + ); + $info = $entryKind === 'place' ? 'Ort aktualisiert.' : 'Veranstaltung aktualisiert.'; + } else { + $listingCatalog->submitUpdateRequest( + $userId, + (int)($_POST['listing_id'] ?? 0), + $payload, + (string)($_POST['moderation_reason'] ?? '') + ); + $info = $entryKind === 'place' ? 'Änderungsanfrage für den Ort eingereicht.' : 'Änderungsanfrage für die Veranstaltung eingereicht.'; + } } else { $createdListingId = $listingCatalog->submitCreateSuggestion($userId, $payload); $info = $entryKind === 'place' ? 'Ort zur Freigabe eingereicht.' : 'Veranstaltung zur Freigabe eingereicht.'; @@ -957,7 +972,13 @@ final class AccountPages $editListingId = (int)$_GET['edit_listing']; if ($editListingId > 0) { $candidate = $listingCatalog->getMemberEntry($editListingId); - if (is_array($candidate) && (string)($candidate['status'] ?? '') === 'published') { + $canEditCandidateDirectly = is_array($candidate) + && (string)($candidate['status'] ?? '') !== 'published' + && ( + (int)($candidate['created_by'] ?? 0) === $userId + || $canReviewListings + ); + if (is_array($candidate) && ((string)($candidate['status'] ?? '') === 'published' || $canEditCandidateDirectly)) { $editListing = $candidate; } else { $error = 'Der gewünschte Eintrag ist für eine Änderungsanfrage nicht verfügbar.'; @@ -1082,6 +1103,7 @@ final class AccountPages 'communityLevelDefinitions', 'communityLevelRightDefinitions', 'communityRoles', + 'canReviewListings', 'systemRoleAssignments', 'recentHighLevelUsers', 'recentHighLevelThresholdLabel', diff --git a/src/App/CommunityAccess.php b/src/App/CommunityAccess.php index fa2bd5d..b64982f 100644 --- a/src/App/CommunityAccess.php +++ b/src/App/CommunityAccess.php @@ -86,10 +86,7 @@ final class CommunityAccess public function canReviewListings(int $userId, ?float $points = null): bool { - if ($this->hasRole($userId, 'owner')) { - return false; - } - if ($this->hasRole($userId, 'site_admin') || $this->hasRole($userId, 'forum_admin')) { + if ($this->hasRole($userId, 'owner') || $this->hasRole($userId, 'site_admin') || $this->hasRole($userId, 'forum_admin')) { return true; } diff --git a/src/App/ListingCatalog.php b/src/App/ListingCatalog.php index 76a7f7c..daa397e 100644 --- a/src/App/ListingCatalog.php +++ b/src/App/ListingCatalog.php @@ -649,7 +649,7 @@ final class ListingCatalog { $this->ensureSchema(); $stmt = $this->pdo->prepare( - 'SELECT l.*, lp.title AS place_title, lp.description AS place_description, lp.street, lp.zip, lp.city, lp.region, lp.lat, lp.lng, lp.place_kind, lp.phone, lp.website_url, lp.opening_hours_note, lp.opening_hours_json, + 'SELECT l.*, lp.status AS place_status, lp.title AS place_title, lp.description AS place_description, lp.street, lp.zip, lp.city, lp.region, lp.lat, lp.lng, lp.place_kind, lp.phone, lp.website_url, lp.opening_hours_note, lp.opening_hours_json, lo.id AS occurrence_id, lo.starts_at, lo.ends_at, lo.occurrence_type, lo.recurrence_rule, lo.recurrence_until, lo.capacity_total, lc.slug AS category_slug @@ -850,6 +850,27 @@ final class ListingCatalog return $this->createModerationRequest($listingId, 'delete', $userId, $reason, null); } + public function updateEditableSuggestion(int $listingId, array $data): void + { + $this->ensureSchema(); + $entry = $this->getMemberEntry($listingId); + if (!$entry) { + throw new \RuntimeException('Eintrag nicht gefunden.'); + } + + $listingStatus = (string)($entry['status'] ?? 'draft'); + if ($listingStatus === 'published') { + throw new \RuntimeException('Veröffentlichte Einträge dürfen nicht direkt überschrieben werden.'); + } + + $placeStatus = (string)($entry['place_status'] ?? 'draft'); + $normalizedListingStatus = in_array($listingStatus, ['draft', 'archived'], true) ? $listingStatus : 'draft'; + $normalizedPlaceStatus = in_array($placeStatus, ['draft', 'archived'], true) ? $placeStatus : $normalizedListingStatus; + + $this->assertNoDuplicateEntry($data, $listingId); + $this->saveDashboardEntry((int)($entry['created_by'] ?? 0), $data, $listingId, $normalizedListingStatus, $normalizedPlaceStatus); + } + public function listOpenModerationRequests(): array { $this->ensureSchema();