This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user