adsad
All checks were successful
Deploy / deploy (push) Successful in 1m2s

This commit is contained in:
2026-08-04 22:07:29 +02:00
parent ac32727dc6
commit 565d197bb2
6 changed files with 143 additions and 103 deletions

View File

@@ -22,6 +22,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- Die Konto-E-Mail wird app-seitig verschlüsselt gespeichert und über einen separaten HMAC-Lookup-Hash adressiert. - Die Konto-E-Mail wird app-seitig verschlüsselt gespeichert und über einen separaten HMAC-Lookup-Hash adressiert.
- Profiladresse kann per Suche oder Browser-Standort übernommen werden. - Profiladresse kann per Suche oder Browser-Standort übernommen werden.
- Im Bereich `Events` gibt es jetzt drei Eintragstypen: eigenes Event, Ort und sonstige Veranstaltung. - Im Bereich `Events` gibt es jetzt drei Eintragstypen: eigenes Event, Ort und sonstige Veranstaltung.
- Die Eingabe im Bereich `Events` soll so einfach wie möglich bleiben und zeigt deshalb zuerst nur Pflichtangaben.
- Eigene Events unterstützen Kategorie, optionales Bild, Ja/Nein-Angabe `Mit Kindern`, optionale Platzzahl und Sichtbarkeit. - Eigene Events unterstützen Kategorie, optionales Bild, Ja/Nein-Angabe `Mit Kindern`, optionale Platzzahl und Sichtbarkeit.
- `Ort` nutzt die gleiche Adresslogik wie das Profil, blendet den Event-Termin aus und erlaubt stattdessen flexible Öffnungs-/Zeitfenster, optionale Kontaktangaben und flexible Preisregeln. - `Ort` nutzt die gleiche Adresslogik wie das Profil, blendet den Event-Termin aus und erlaubt stattdessen flexible Öffnungs-/Zeitfenster, optionale Kontaktangaben und flexible Preisregeln.
- `Sonstige Veranstaltung` nutzt ebenfalls die Profil-Adresslogik, unterstützt Wiederholungen, ein Pflichtfeld `Gültig bis`, flexible Preisregeln, Bild-Upload und die Angabe `Mit Kindern`. - `Sonstige Veranstaltung` nutzt ebenfalls die Profil-Adresslogik, unterstützt Wiederholungen, ein Pflichtfeld `Gültig bis`, flexible Preisregeln, Bild-Upload und die Angabe `Mit Kindern`.

View File

@@ -25,6 +25,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
- Mitgliederbereich `Events` unterstützt jetzt drei Anlagetypen: `Eigenes Event`, `Ort`, `Sonstige Veranstaltung` - Mitgliederbereich `Events` unterstützt jetzt drei Anlagetypen: `Eigenes Event`, `Ort`, `Sonstige Veranstaltung`
- Eigene Events haben jetzt Kategorie, optionales Bild, klare Kinderangabe, optionale Platzzahl und serverseitig validierte Adressdaten - Eigene Events haben jetzt Kategorie, optionales Bild, klare Kinderangabe, optionale Platzzahl und serverseitig validierte Adressdaten
- Orte und sonstige Veranstaltungen haben jetzt eine angepasste Eingabemaske mit Ortssuche, Browser-Adressübernahme, serverseitiger Adressvalidierung, flexiblen Preisregeln, flexiblen Öffnungs-/Zeitfenstern und Bild-Upload - Orte und sonstige Veranstaltungen haben jetzt eine angepasste Eingabemaske mit Ortssuche, Browser-Adressübernahme, serverseitiger Adressvalidierung, flexiblen Preisregeln, flexiblen Öffnungs-/Zeitfenstern und Bild-Upload
- Die Eingabemasken sind jetzt progressiv aufgebaut: erst Pflichtangaben, optionale Daten in einklappbaren Bereichen
## Wording-Regel ## Wording-Regel
- Primärbegriff im Produkt: `Events` - Primärbegriff im Produkt: `Events`

View File

@@ -22,6 +22,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers. Core areas are local events
- Account email is encrypted application-side and addressed through a separate HMAC lookup hash. - Account email is encrypted application-side and addressed through a separate HMAC lookup hash.
- Profile address can be completed via address search or browser-based location import. - Profile address can be completed via address search or browser-based location import.
- The `Events` area now distinguishes between own events, places, and other time-limited event entries. - The `Events` area now distinguishes between own events, places, and other time-limited event entries.
- The `Events` input flow should stay as simple as possible and therefore shows required fields first.
- Own events support category, optional image, a clear `with children` yes/no field, optional capacity, and visibility. - Own events support category, optional image, a clear `with children` yes/no field, optional capacity, and visibility.
- `Place` uses the same address flow as the profile, hides the event date, and instead supports flexible opening/time slots, optional contact data, and flexible pricing rules. - `Place` uses the same address flow as the profile, hides the event date, and instead supports flexible opening/time slots, optional contact data, and flexible pricing rules.
- `Other Event` also uses the profile-style address flow, supports recurrence, a required `valid until` field, flexible pricing rules, image upload, and a `with children` flag. - `Other Event` also uses the profile-style address flow, supports recurrence, a required `valid until` field, flexible pricing rules, image upload, and a `with children` flag.

View File

@@ -25,6 +25,7 @@ Papa-Kind-Treff is a PHP-based platform for fathers focused on local events, app
- member area `Events` now supports three creation types: `Own Event`, `Place`, and `Other Event` - member area `Events` now supports three creation types: `Own Event`, `Place`, and `Other Event`
- own events now include category, optional image, explicit child suitability, optional capacity, and server-side validated address data - own events now include category, optional image, explicit child suitability, optional capacity, and server-side validated address data
- places and other events now use adaptive forms with place lookup, browser address pickup, server-side address validation, flexible pricing rules, flexible opening/time slots, and image upload - places and other events now use adaptive forms with place lookup, browser address pickup, server-side address validation, flexible pricing rules, flexible opening/time slots, and image upload
- the member-area entry forms now follow a progressive approach: required fields first, optional data inside collapsible sections
## Core Wording Rule ## Core Wording Rule
- Primary product term: `Events` - Primary product term: `Events`

View File

@@ -39,6 +39,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf:
- Im Mitgliederbereich unter `Events` können Nutzer jetzt zwischen `Eigenes Event`, `Ort` und `sonstiger Veranstaltung` unterscheiden - Im Mitgliederbereich unter `Events` können Nutzer jetzt zwischen `Eigenes Event`, `Ort` und `sonstiger Veranstaltung` unterscheiden
- Für Orte und sonstige Veranstaltungen gibt es jetzt typabhängige Masken mit Ortssuche, Browser-Adressübernahme, Adressprüfung, flexiblen Preisregeln, flexiblen Öffnungs-/Zeitfenstern und Bild-Upload - Für Orte und sonstige Veranstaltungen gibt es jetzt typabhängige Masken mit Ortssuche, Browser-Adressübernahme, Adressprüfung, flexiblen Preisregeln, flexiblen Öffnungs-/Zeitfenstern und Bild-Upload
- Eigene Events unterstützen jetzt zusätzlich Kategorie, Bild-Upload, klare Kinderangabe und optionale Platzzahl - Eigene Events unterstützen jetzt zusätzlich Kategorie, Bild-Upload, klare Kinderangabe und optionale Platzzahl
- Die Eingabe im Mitgliederbereich ist jetzt bewusst vereinfacht: zuerst nur Pflichtangaben, optionale Angaben in einklappbaren Bereichen
## Datenschutz und Sicherheit ## Datenschutz und Sicherheit
- sensible Profilfelder werden app-seitig verschlüsselt gespeichert - sensible Profilfelder werden app-seitig verschlüsselt gespeichert

View File

@@ -921,6 +921,9 @@ if (!empty($canManageSystemSettings)) {
</p> </p>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="card" id="entryKindQuickGuide">
<p class="muted small" style="margin:0;">Trage zuerst nur die wichtigsten Angaben ein. Alles Weitere ist optional und unten einklappbar.</p>
</div>
<div class="stack gap-6"> <div class="stack gap-6">
<label class="label" for="evTitle">Titel</label> <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) ?>"> <input id="evTitle" name="title" class="input" placeholder="z. B. Väter-Kaffee im Park" required value="<?= htmlspecialchars($titleValue, ENT_QUOTES) ?>">
@@ -947,20 +950,16 @@ if (!empty($canManageSystemSettings)) {
<input id="categoryCustomTitle" name="category_custom_title" class="input" placeholder="z. B. Tierpark"> <input id="categoryCustomTitle" name="category_custom_title" class="input" placeholder="z. B. Tierpark">
</div> </div>
</div> </div>
<div class="stack gap-6" id="placeLookupWrap"> <details id="placeLookupWrap" class="card">
<summary style="cursor:pointer; font-weight:600;">Vorhandenen Ort übernehmen</summary>
<div class="stack gap-6" style="margin-top:12px;">
<label class="label" for="existingPlaceLookup">Vorhandenen Ort suchen (optional)</label> <label class="label" for="existingPlaceLookup">Vorhandenen Ort suchen (optional)</label>
<div class="flex gap-8" style="flex-wrap:wrap;"> <div class="flex gap-8" style="flex-wrap:wrap;">
<input id="existingPlaceLookup" class="input" list="existingPlacesList" placeholder="z. B. Tierpark, Café, Spielplatz" style="flex:1;"> <input id="existingPlaceLookup" class="input" list="existingPlacesList" placeholder="z. B. Tierpark, Café, Spielplatz" style="flex:1;">
<button class="btn ghost" type="button" id="btnUseExistingPlace">Ort übernehmen</button> <button class="btn ghost" type="button" id="btnUseExistingPlace">Ort übernehmen</button>
</div> </div>
</div> </div>
<div class="stack gap-6"> </details>
<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>
<div class="form-grid" id="eventDateRow"> <div class="form-grid" id="eventDateRow">
<div class="stack gap-6"> <div class="stack gap-6">
<label class="label" for="evDate">Start / Termin</label> <label class="label" for="evDate">Start / Termin</label>
@@ -1030,14 +1029,36 @@ if (!empty($canManageSystemSettings)) {
<div class="flex gap-8" style="flex-wrap:wrap; align-items:center;"> <div class="flex gap-8" style="flex-wrap:wrap; align-items:center;">
<button class="btn ghost" type="button" id="btnEventAddressSearch">Adresse suchen</button> <button class="btn ghost" type="button" id="btnEventAddressSearch">Adresse suchen</button>
<button class="btn ghost" type="button" id="btnEventBrowserLocation">Browser-Adresse übernehmen</button> <button class="btn ghost" type="button" id="btnEventBrowserLocation">Browser-Adresse übernehmen</button>
<button class="btn ghost" type="button" id="btnAddrToMap">Adresse auf Karte setzen</button>
<span class="muted small">Hält Karte und Adresse synchron.</span>
</div> </div>
<div id="eventAddressStatus" class="muted small" style="margin:0;"></div> <div id="eventAddressStatus" class="muted small" style="margin:0;"></div>
<input type="hidden" id="evLat" name="lat" value="<?= htmlspecialchars($latValue, ENT_QUOTES) ?>"> <input type="hidden" id="evLat" name="lat" value="<?= htmlspecialchars($latValue, ENT_QUOTES) ?>">
<input type="hidden" id="evLng" name="lng" value="<?= htmlspecialchars($lngValue, ENT_QUOTES) ?>"> <input type="hidden" id="evLng" name="lng" value="<?= htmlspecialchars($lngValue, ENT_QUOTES) ?>">
<div class="stack gap-6" id="placeContactWrap"> <details class="card" id="mapDetails">
<div class="form-grid"> <summary style="cursor:pointer; font-weight:600;">Adresse auf Karte wählen</summary>
<div class="stack gap-6" style="margin-top:12px;">
<div class="flex gap-8" style="flex-wrap:wrap; align-items:center;">
<button class="btn ghost" type="button" id="btnAddrToMap">Adresse auf Karte setzen</button>
<span class="muted small">Hält Karte und Adresse synchron.</span>
</div>
<div class="stack gap-6">
<button class="btn ghost" type="button" id="btnMap">Auf Karte suchen</button>
<div id="mapWrapper" class="map-wrapper" hidden>
<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. Zoom/Scroll möglich.</p>
</div>
<div id="mapContainer" class="map-container"></div>
</div>
</div>
</div>
</details>
<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"> <div class="stack gap-6">
<label class="label" for="placePhone">Telefon (optional)</label> <label class="label" for="placePhone">Telefon (optional)</label>
<input id="placePhone" name="phone" class="input" value="<?= htmlspecialchars($phoneValue, ENT_QUOTES) ?>"> <input id="placePhone" name="phone" class="input" value="<?= htmlspecialchars($phoneValue, ENT_QUOTES) ?>">
@@ -1047,8 +1068,10 @@ if (!empty($canManageSystemSettings)) {
<input id="placeWebsite" name="website_url" class="input" type="url" placeholder="https://..." value="<?= htmlspecialchars($websiteValue, ENT_QUOTES) ?>"> <input id="placeWebsite" name="website_url" class="input" type="url" placeholder="https://..." value="<?= htmlspecialchars($websiteValue, ENT_QUOTES) ?>">
</div> </div>
</div> </div>
</div> </details>
<div class="stack gap-6" id="placeOpeningHoursWrap"> <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"> <div class="flex between center-y">
<label class="label" for="openingHoursNote">Öffnungszeiten / Zeitfenster</label> <label class="label" for="openingHoursNote">Öffnungszeiten / Zeitfenster</label>
<button class="btn ghost" type="button" id="btnAddOpeningRow">Zeit hinzufügen</button> <button class="btn ghost" type="button" id="btnAddOpeningRow">Zeit hinzufügen</button>
@@ -1065,21 +1088,10 @@ if (!empty($canManageSystemSettings)) {
</div> </div>
<textarea id="openingHoursNote" name="opening_hours_note" class="textarea" rows="2" placeholder="Freitext-Hinweis zu Öffnungszeiten oder Besonderheiten"><?= htmlspecialchars($openingHoursValue, ENT_QUOTES) ?></textarea> <textarea id="openingHoursNote" name="opening_hours_note" class="textarea" rows="2" placeholder="Freitext-Hinweis zu Öffnungszeiten oder Besonderheiten"><?= htmlspecialchars($openingHoursValue, ENT_QUOTES) ?></textarea>
</div> </div>
<div class="stack gap-6"> </details>
<button class="btn ghost" type="button" id="btnMap">Auf Karte suchen</button> <details class="card" id="visibilityDetails">
<div id="mapWrapper" class="map-wrapper" hidden> <summary style="cursor:pointer; font-weight:600;">Weitere Optionen</summary>
<div class="form-row"> <div class="form-grid" style="margin-top:12px;">
<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. Zoom/Scroll möglich.</p>
</div>
<div id="mapContainer" class="map-container"></div>
</div>
</div>
<div class="form-grid">
<div class="stack gap-6"> <div class="stack gap-6">
<label class="label" for="evVis">Sichtbarkeit</label> <label class="label" for="evVis">Sichtbarkeit</label>
<select id="evVis" name="visibility" class="select"> <select id="evVis" name="visibility" class="select">
@@ -1088,9 +1100,22 @@ if (!empty($canManageSystemSettings)) {
</select> </select>
</div> </div>
</div> </div>
<div class="stack gap-10" id="eventPricingWrap"> </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"> <div class="flex between center-y">
<label class="label">Eintritt / Kosten</label> <label class="label">Preisregeln</label>
<button class="btn ghost" type="button" id="btnAddPriceRow">Preisregel hinzufügen</button> <button class="btn ghost" type="button" id="btnAddPriceRow">Preisregel hinzufügen</button>
</div> </div>
<div id="priceRows" class="stack gap-8"> <div id="priceRows" class="stack gap-8">
@@ -1128,6 +1153,7 @@ if (!empty($canManageSystemSettings)) {
<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> <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>
</div> </div>
</details>
<div class="flex gap-12"> <div class="flex gap-12">
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button> <button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
<button class="btn" type="submit"><?= htmlspecialchars($submitLabel, ENT_QUOTES) ?></button> <button class="btn" type="submit"><?= htmlspecialchars($submitLabel, ENT_QUOTES) ?></button>
@@ -1229,6 +1255,15 @@ if (!empty($canManageSystemSettings)) {
}; };
} }
function setSectionState(element, visible) {
if (!element) return;
element.hidden = !visible;
element.querySelectorAll('input, select, textarea, button').forEach((field) => {
if (field.type === 'hidden') return;
field.disabled = !visible;
});
}
function syncEventEntryKind(nextKind) { function syncEventEntryKind(nextKind) {
const kind = nextKind || entryKindInput?.value || 'event'; const kind = nextKind || entryKindInput?.value || 'event';
if (entryKindInput) entryKindInput.value = kind; if (entryKindInput) entryKindInput.value = kind;
@@ -1249,17 +1284,17 @@ if (!empty($canManageSystemSettings)) {
} }
} }
if (eventDateRow) eventDateRow.hidden = kind === 'place'; setSectionState(eventDateRow, kind !== 'place');
if (eventDateInput) eventDateInput.required = kind !== 'place'; if (eventDateInput) eventDateInput.required = kind !== 'place';
if (eventLocationLabelWrap) eventLocationLabelWrap.hidden = kind !== 'event'; setSectionState(eventLocationLabelWrap, kind === 'event');
if (categorySelect) categorySelect.required = true; if (categorySelect) categorySelect.required = true;
if (eventChildrenWrap) eventChildrenWrap.hidden = kind === 'place'; setSectionState(eventChildrenWrap, kind === 'event' || kind === 'editorial_event');
if (eventCapacityWrap) eventCapacityWrap.hidden = kind !== 'event'; setSectionState(eventCapacityWrap, kind === 'event');
if (eventPricingWrap) eventPricingWrap.hidden = kind === 'event'; setSectionState(eventPricingWrap, kind === 'place' || kind === 'editorial_event');
if (placeLookupWrap) placeLookupWrap.hidden = kind === 'event'; setSectionState(placeLookupWrap, kind === 'place' || kind === 'editorial_event');
if (placeContactWrap) placeContactWrap.hidden = kind === 'event'; setSectionState(placeContactWrap, kind === 'place' || kind === 'editorial_event');
if (placeOpeningHoursWrap) placeOpeningHoursWrap.hidden = kind === 'event'; setSectionState(placeOpeningHoursWrap, kind === 'place' || kind === 'editorial_event');
if (eventRecurrenceWrap) eventRecurrenceWrap.hidden = kind !== 'editorial_event'; setSectionState(eventRecurrenceWrap, kind === 'editorial_event');
syncRecurrenceVisibility(); syncRecurrenceVisibility();
} }