asdasd
All checks were successful
Deploy / deploy (push) Successful in 58s

This commit is contained in:
2026-07-29 22:54:50 +02:00
parent d26f4a5792
commit 76591569b6
5 changed files with 115 additions and 22 deletions

View File

@@ -13,6 +13,7 @@ Aktueller Projektstand
- Gespeicherte Benutzer-Avatare werden lokal unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt und bei Änderungen überschrieben. - Gespeicherte Benutzer-Avatare werden lokal unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt und bei Änderungen überschrieben.
- Der Generator besitzt einen Button `Random Profilbild`, der Seed und Varianten des aktiven Styles zufällig neu setzt. - Der Generator besitzt einen Button `Random Profilbild`, der Seed und Varianten des aktiven Styles zufällig neu setzt.
- Beim Oeffnen des Modals werden nur die Bilder des aktiven Styles und Tabs geladen; weitere Tabs laden ihre Varianten erst beim Wechsel nach. - Beim Oeffnen des Modals werden nur die Bilder des aktiven Styles und Tabs geladen; weitere Tabs laden ihre Varianten erst beim Wechsel nach.
- Kinder koennen im Mitgliederbereich zusaetzlich nachtraeglich bearbeitet und geloescht werden.
- Alte statische Avatar-Presets und frühere Layer-Assets sind entfernt. - Alte statische Avatar-Presets und frühere Layer-Assets sind entfernt.
- Änderungen an Funktionen mit Cookies, LocalStorage, SessionStorage, Geolocation, Tracking oder Drittanbietern müssen immer auch in Datenschutz-/Cookie-Hinweisen und im Consent-Flow berücksichtigt werden. - Änderungen an Funktionen mit Cookies, LocalStorage, SessionStorage, Geolocation, Tracking oder Drittanbietern müssen immer auch in Datenschutz-/Cookie-Hinweisen und im Consent-Flow berücksichtigt werden.

View File

@@ -33,6 +33,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons. - Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons.
- Das Profil-Menü enthält aktuell `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`, `Abmelden`. - Das Profil-Menü enthält aktuell `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`, `Abmelden`.
- Der Mitgliederbereich selbst ist als Layout mit linker Bereichsnavigation aufgebaut. - Der Mitgliederbereich selbst ist als Layout mit linker Bereichsnavigation aufgebaut.
- Kinder koennen im Mitgliederbereich angelegt, nachtraeglich bearbeitet und wieder geloescht werden.
- Im Profil kann der Nutzer aktuell Komponenten wie Augen, Augenbrauen, Mund, Brille, Haare, Bart und Ohrringe direkt per Vorschaubild auswählen. - Im Profil kann der Nutzer aktuell Komponenten wie Augen, Augenbrauen, Mund, Brille, Haare, Bart und Ohrringe direkt per Vorschaubild auswählen.
- Der Button `Random Profilbild` setzt Seed und Varianten des aktiven Styles zufaellig neu und aktualisiert die Vorschau sofort. - Der Button `Random Profilbild` setzt Seed und Varianten des aktiven Styles zufaellig neu und aktualisiert die Vorschau sofort.
- Beim Oeffnen des Avatar-Generators wird nur der aktive Style-/Komponenten-Tab geladen; weitere Varianten werden erst beim Wechsel nachgeladen. - Beim Oeffnen des Avatar-Generators wird nur der aktive Style-/Komponenten-Tab geladen; weitere Varianten werden erst beim Wechsel nachgeladen.

View File

@@ -27,6 +27,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
- Navigation aktuell mit `Home`, `Suche`, `Community` - Navigation aktuell mit `Home`, `Suche`, `Community`
- eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen` - eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`
- der Mitgliederbereich ist als Seitenlayout mit linker Bereichsnavigation aufgebaut - der Mitgliederbereich ist als Seitenlayout mit linker Bereichsnavigation aufgebaut
- im Bereich `Kinder` koennen vorhandene Eintraege jetzt auch nachtraeglich bearbeitet und geloescht werden
- Debug-Floating-Button nur für `site_admin` im Debug-Modus - Debug-Floating-Button nur für `site_admin` im Debug-Modus
## Technik ## Technik

View File

@@ -10,6 +10,9 @@ $editing = isset($editEvent) && $editEvent !== null;
$actionEvent = $editing ? 'event_update' : 'event_add'; $actionEvent = $editing ? 'event_update' : 'event_add';
$startVal = $editEvent ? date('Y-m-d\TH:i', strtotime((string)$editEvent['starts_at'])) : ''; $startVal = $editEvent ? date('Y-m-d\TH:i', strtotime((string)$editEvent['starts_at'])) : '';
$allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false; $allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false;
$editingChild = isset($editChild) && $editChild !== null;
$childAction = $editingChild ? 'child_update' : 'child_add';
$childModalTitle = $editingChild ? 'Kind bearbeiten' : 'Kind hinzufügen';
$sectionLinks = [ $sectionLinks = [
'profile' => 'Profil', 'profile' => 'Profil',
'children' => 'Kinder', 'children' => 'Kinder',
@@ -104,12 +107,37 @@ $sectionLinks = [
<?php else: ?> <?php else: ?>
<ul class="dash-list"> <ul class="dash-list">
<?php foreach ($children as $c): ?> <?php foreach ($children as $c): ?>
<li><?= htmlspecialchars($c['first_name'], ENT_QUOTES) ?>, <?= htmlspecialchars($c['gender'], ENT_QUOTES) ?> <?= $c['age_years'] ? '(' . (int)$c['age_years'] . ' Jahre)' : '' ?></li> <li>
<div style="display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap:wrap;">
<div>
<strong><?= htmlspecialchars($c['first_name'], ENT_QUOTES) ?></strong>,
<?= htmlspecialchars($c['gender'], ENT_QUOTES) ?>
<?= $c['age_years'] ? '(' . (int)$c['age_years'] . ' Jahre)' : '' ?>
<?php if (!empty($c['birthdate'])): ?>
<span class="muted small">· geboren am <?= htmlspecialchars(date('d.m.Y', strtotime((string)$c['birthdate'])), ENT_QUOTES) ?></span>
<?php endif; ?>
<?php if (!empty($c['note'])): ?>
<div class="muted small" style="margin-top:4px;"><?= htmlspecialchars($c['note'], ENT_QUOTES) ?></div>
<?php endif; ?>
</div>
<div class="flex gap-12" style="flex-wrap:wrap;">
<a class="btn ghost" href="/dashboard?section=children&edit_child=<?= (int)$c['id'] ?>">Bearbeiten</a>
<form method="post" action="/dashboard?section=children" onsubmit="return confirm('Kind wirklich löschen?');">
<input type="hidden" name="action" value="child_delete">
<input type="hidden" name="child_id" value="<?= (int)$c['id'] ?>">
<button class="btn ghost" type="submit">Löschen</button>
</form>
</div>
</div>
</li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
<?php endif; ?> <?php endif; ?>
<div class="flex gap-12" style="margin-top:18px;"> <div class="flex gap-12" style="margin-top:18px;">
<button class="btn" type="button" data-modal-open="modalChild">Kind hinzufügen</button> <button class="btn" type="button" data-modal-open="modalChild">Kind hinzufügen</button>
<?php if ($editingChild): ?>
<a class="btn ghost" href="/dashboard?section=children">Bearbeitung abbrechen</a>
<?php endif; ?>
</div> </div>
</div> </div>
</section> </section>
@@ -524,43 +552,46 @@ $sectionLinks = [
<div class="modal" id="modalChild"> <div class="modal" id="modalChild">
<div class="panel"> <div class="panel">
<div class="head flex between center-y"> <div class="head flex between center-y">
<h3>Kind hinzufügen</h3> <h3><?= htmlspecialchars($childModalTitle, ENT_QUOTES) ?></h3>
<button class="btn ghost" type="button" data-modal-close>✕</button> <button class="btn ghost" type="button" data-modal-close>✕</button>
</div> </div>
<form method="post" class="stack gap-12"> <form method="post" class="stack gap-12">
<input type="hidden" name="action" value="child_add"> <input type="hidden" name="action" value="<?= htmlspecialchars($childAction, ENT_QUOTES) ?>">
<?php if ($editingChild): ?>
<input type="hidden" name="child_id" value="<?= (int)$editChild['id'] ?>">
<?php endif; ?>
<div class="form-grid"> <div class="form-grid">
<div class="stack gap-6"> <div class="stack gap-6">
<label class="label" for="cName">Vorname</label> <label class="label" for="cName">Vorname</label>
<input id="cName" name="first_name" class="input" required> <input id="cName" name="first_name" class="input" value="<?= htmlspecialchars((string)($editChild['first_name'] ?? ''), ENT_QUOTES) ?>" required>
</div> </div>
<div class="stack gap-6"> <div class="stack gap-6">
<label class="label" for="cGender">Geschlecht</label> <label class="label" for="cGender">Geschlecht</label>
<select id="cGender" name="gender" class="select"> <select id="cGender" name="gender" class="select">
<option value="male">Männlich</option> <option value="male" <?= (($editChild['gender'] ?? 'unknown') === 'male') ? 'selected' : '' ?>>Männlich</option>
<option value="female">Weiblich</option> <option value="female" <?= (($editChild['gender'] ?? 'unknown') === 'female') ? 'selected' : '' ?>>Weiblich</option>
<option value="diverse">Divers</option> <option value="diverse" <?= (($editChild['gender'] ?? 'unknown') === 'diverse') ? 'selected' : '' ?>>Divers</option>
<option value="unknown">Unbekannt</option> <option value="unknown" <?= (($editChild['gender'] ?? 'unknown') === 'unknown') ? 'selected' : '' ?>>Unbekannt</option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-grid"> <div class="form-grid">
<div class="stack gap-6"> <div class="stack gap-6">
<label class="label" for="cBirth">Geburtsdatum</label> <label class="label" for="cBirth">Geburtsdatum</label>
<input id="cBirth" name="birthdate" class="input" type="date"> <input id="cBirth" name="birthdate" class="input" type="date" value="<?= htmlspecialchars((string)($editChild['birthdate'] ?? ''), ENT_QUOTES) ?>">
</div> </div>
<div class="stack gap-6"> <div class="stack gap-6">
<label class="label" for="cAge">Alter (Jahre)</label> <label class="label" for="cAge">Alter (Jahre)</label>
<input id="cAge" name="age_years" class="input" type="number" min="0" max="18"> <input id="cAge" name="age_years" class="input" type="number" min="0" max="18" value="<?= htmlspecialchars((string)($editChild['age_years'] ?? ''), ENT_QUOTES) ?>">
</div> </div>
</div> </div>
<div class="stack gap-6"> <div class="stack gap-6">
<label class="label" for="cNote">Notiz</label> <label class="label" for="cNote">Notiz</label>
<input id="cNote" name="note" class="input"> <input id="cNote" name="note" class="input" value="<?= htmlspecialchars((string)($editChild['note'] ?? ''), ENT_QUOTES) ?>">
</div> </div>
<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">Speichern</button> <button class="btn" type="submit"><?= $editingChild ? 'Änderungen speichern' : 'Speichern' ?></button>
</div> </div>
</form> </form>
</div> </div>
@@ -840,6 +871,15 @@ $sectionLinks = [
})(); })();
<?php endif; ?> <?php endif; ?>
<?php if ($editingChild): ?>
(function(){
const modal = document.getElementById('modalChild');
if (modal) {
modal.classList.add('open');
}
})();
<?php endif; ?>
(function(){ (function(){
const statusText = document.getElementById('locationBrowserStatusText'); const statusText = document.getElementById('locationBrowserStatusText');
const hintText = document.getElementById('locationBrowserStatusHint'); const hintText = document.getElementById('locationBrowserStatusHint');

View File

@@ -207,19 +207,63 @@ final class AccountPages
); );
} }
$info = 'Einstellungen gespeichert.'; $info = 'Einstellungen gespeichert.';
} elseif ($action === 'child_add') { } elseif ($action === 'child_add' || $action === 'child_update') {
$firstNameEnc = $crypto ? $crypto->encrypt(trim((string)$_POST['first_name'])) : trim((string)$_POST['first_name']); $childId = (int)($_POST['child_id'] ?? 0);
$noteEnc = $crypto ? $crypto->encrypt(trim((string)$_POST['note'])) : trim((string)$_POST['note']); $firstName = trim((string)($_POST['first_name'] ?? ''));
$stmt = $pdo?->prepare('INSERT INTO children (user_id, gender, birthdate, age_years, encrypted_first_name, note, created_at, updated_at) VALUES (:uid, :gender, :birthdate, :age, :name, :note, NOW(), NOW())'); $gender = (string)($_POST['gender'] ?? 'unknown');
$birthdate = trim((string)($_POST['birthdate'] ?? ''));
$ageYearsRaw = trim((string)($_POST['age_years'] ?? ''));
$ageYears = $ageYearsRaw !== '' ? max(0, min(18, (int)$ageYearsRaw)) : null;
$note = trim((string)($_POST['note'] ?? ''));
if ($firstName === '') {
throw new \RuntimeException('Bitte gib einen Vornamen an.');
}
if (!in_array($gender, ['male', 'female', 'diverse', 'unknown'], true)) {
$gender = 'unknown';
}
$firstNameEnc = $crypto ? $crypto->encrypt($firstName) : $firstName;
$noteEnc = $crypto ? $crypto->encrypt($note) : $note;
if ($action === 'child_add') {
$stmt = $pdo?->prepare('INSERT INTO children (user_id, gender, birthdate, age_years, encrypted_first_name, note, created_at, updated_at) VALUES (:uid, :gender, :birthdate, :age, :name, :note, NOW(), NOW())');
$stmt?->execute([
'uid' => $userId,
'gender' => $gender,
'birthdate' => $birthdate !== '' ? $birthdate : null,
'age' => $ageYears,
'name' => $firstNameEnc,
'note' => $noteEnc,
]);
$info = 'Kind hinzugefügt.';
} else {
$stmt = $pdo?->prepare('UPDATE children SET gender = :gender, birthdate = :birthdate, age_years = :age, encrypted_first_name = :name, note = :note, updated_at = NOW() WHERE id = :childId AND user_id = :uid');
$stmt?->execute([
'uid' => $userId,
'childId' => $childId,
'gender' => $gender,
'birthdate' => $birthdate !== '' ? $birthdate : null,
'age' => $ageYears,
'name' => $firstNameEnc,
'note' => $noteEnc,
]);
if ($stmt && $stmt->rowCount() < 1) {
throw new \RuntimeException('Kind nicht gefunden.');
}
$info = 'Kind gespeichert.';
}
} elseif ($action === 'child_delete') {
$childId = (int)($_POST['child_id'] ?? 0);
$stmt = $pdo?->prepare('DELETE FROM children WHERE id = :childId AND user_id = :uid');
$stmt?->execute([ $stmt?->execute([
'childId' => $childId,
'uid' => $userId, 'uid' => $userId,
'gender' => $_POST['gender'] ?? 'unknown',
'birthdate' => $_POST['birthdate'] ?: null,
'age' => $_POST['age_years'] ?: null,
'name' => $firstNameEnc,
'note' => $noteEnc,
]); ]);
$info = 'Kind hinzugefügt.'; if ($stmt && $stmt->rowCount() < 1) {
throw new \RuntimeException('Kind nicht gefunden.');
}
$info = 'Kind gelöscht.';
} elseif ($action === 'event_add' || $action === 'event_update') { } elseif ($action === 'event_add' || $action === 'event_update') {
$street = trim((string)($_POST['street'] ?? '')); $street = trim((string)($_POST['street'] ?? ''));
$zip = trim((string)($_POST['zip'] ?? '')); $zip = trim((string)($_POST['zip'] ?? ''));
@@ -371,6 +415,8 @@ final class AccountPages
} }
$profile = AvatarManager::normalizeProfile($profile, $userId); $profile = AvatarManager::normalizeProfile($profile, $userId);
$editChildId = isset($_GET['edit_child']) ? (int)$_GET['edit_child'] : 0;
$editChild = null;
$children = []; $children = [];
$stmt = $pdo?->prepare('SELECT id, encrypted_first_name AS first_name, note, gender, birthdate, age_years FROM children WHERE user_id = :id ORDER BY id DESC'); $stmt = $pdo?->prepare('SELECT id, encrypted_first_name AS first_name, note, gender, birthdate, age_years FROM children WHERE user_id = :id ORDER BY id DESC');
$stmt?->execute(['id' => $userId]); $stmt?->execute(['id' => $userId]);
@@ -381,6 +427,9 @@ final class AccountPages
$c['note'] = $crypto->decrypt((string)($c['note'] ?? '')) ?: ''; $c['note'] = $crypto->decrypt((string)($c['note'] ?? '')) ?: '';
} }
$children[] = $c; $children[] = $c;
if ($editChildId > 0 && (int)$c['id'] === $editChildId) {
$editChild = $c;
}
} }
$eventsUpcoming = []; $eventsUpcoming = [];
@@ -434,6 +483,7 @@ final class AccountPages
'error', 'error',
'profile', 'profile',
'children', 'children',
'editChild',
'eventsUpcoming', 'eventsUpcoming',
'eventsPast', 'eventsPast',
'editEvent', 'editEvent',