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)) {
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();