Favoriten überarbeitet

This commit is contained in:
2026-04-11 18:44:25 +02:00
parent 624c4e918e
commit 769ebc3ba8
5 changed files with 377 additions and 41 deletions
+3 -5
View File
@@ -1,8 +1,6 @@
<<<<<<< HEAD
.codex-temp/
php-server.log
php-server.err.log
php-server-error.log
=======
.codex-temp/
php-server-error.log
>>>>>>> fc3264cc2fa76249c40e422de2381eb7c6920c28
__pycache__/
*.pyc
+2
View File
@@ -10,6 +10,8 @@ Hier liegen die internen Anleitungen und technischen Hilfen für das Projekt.
Lokale Startanleitung für den PHP-Webserver auf den bekannten Arbeitsplätzen
- [`docs/README_RECHTESYSTEM.md`](README_RECHTESYSTEM.md)
Beschreibung des aktuellen Rollen- und Seitenrechtesystems
- [`docs/README_DATENBANKABFRAGEN.md`](README_DATENBANKABFRAGEN.md)
Kurzanleitung fuer lokale Datenbankabfragen mit der vorhandenen Projektkonfiguration
## Hinweis
+83
View File
@@ -0,0 +1,83 @@
# Datenbankabfragen
Diese Notiz beschreibt, wie in diesem Projekt lokale Datenbankabfragen ausgefuehrt werden koennen.
## Zugangsdaten
Die Verbindungsdaten liegen in:
- `config/database.php`
Dort stehen `host`, `dbname`, `user`, `pass` und `charset`.
## Empfohlener Weg
Am einfachsten ist eine kurze PHP-Datei oder ein kurzes PHP-Skript, das die vorhandene Projektkonfiguration verwendet.
Beispiel:
```php
<?php
declare(strict_types=1);
$config = require __DIR__ . '/../config/database.php';
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$config['host'],
$config['dbname'],
$config['charset'] ?? 'utf8mb4'
);
$pdo = new PDO(
$dsn,
$config['user'],
$config['pass'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
$stmt = $pdo->query("SELECT id, mp_number, designation_text FROM minorplanets_mpc WHERE mp_number = 1 LIMIT 1");
$row = $stmt->fetch();
echo json_encode($row, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
```
## Ausfuehrung
Wenn `php` nicht im Systempfad liegt, kann die lokale `php.exe` direkt verwendet werden.
Beispiel von diesem Rechner:
```powershell
C:\laragon\bin\php\php-8.4.5-nts-Win32-vs17-x64\php.exe .codex-temp\check_defaults.php
```
## Praktische Beispielabfragen
`Ceres` pruefen:
```sql
SELECT id, mp_number, designation_text, desig_packed
FROM minorplanets_mpc
WHERE mp_number = 1
LIMIT 1;
```
`Halley` pruefen:
```sql
SELECT id, designation_and_name, orbit_type
FROM comets_mpc
WHERE designation_and_name LIKE '%Halley%'
ORDER BY designation_and_name ASC, id ASC
LIMIT 10;
```
## Hinweise
- Temporaere Pruefskripte am besten in `.codex-temp/` anlegen und danach wieder loeschen.
- Fuer Projektcode immer die vorhandene `config/database.php` verwenden und keine Zugangsdaten hart in Dateien eintragen.
- Wenn nur geprueft werden soll, reichen `SELECT`-Abfragen ohne schreibende Operationen.
+205 -36
View File
@@ -358,18 +358,26 @@ function renderTrackedMinorplanetsSection(array $trackedMinorplanets, bool $oob
<td class="<?= (int) $minorplanet['is_favorite'] === 1 ? 'favorite-text' : '' ?>"><?= h($minorplanet['mp_number'] !== null ? (string) $minorplanet['mp_number'] : '-') ?></td>
<td class="<?= (int) $minorplanet['is_favorite'] === 1 ? 'favorite-text' : '' ?>"><?= (int) $minorplanet['is_favorite'] === 1 ? '*' : '-' ?></td>
<td class="action-cell">
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kleinplaneten-Favorit wird aktualisiert..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML">
<input type="hidden" name="action" value="toggle_minorplanet_favorite">
<input type="hidden" name="minorplanet_id" value="<?= (int) $minorplanet['id'] ?>">
<button type="submit" class="btn btn-sm btn-secondary<?= (int) $minorplanet['is_favorite'] === 1 ? ' favorite-active' : '' ?>">
<?= (int) $minorplanet['is_favorite'] === 1 ? 'Favorit entfernen' : 'Als Favorit' ?>
</button>
</form>
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kleinplanet wird entfernt..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML" onsubmit="return confirm('Kleinplanet wirklich entfernen?');">
<input type="hidden" name="action" value="remove_minorplanet">
<input type="hidden" name="minorplanet_id" value="<?= (int) $minorplanet['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Entfernen</button>
</form>
<?php if (count($trackedMinorplanets) <= 1 && (int) $minorplanet['is_favorite'] === 1): ?>
<span class="hint">Favorit bleibt gesetzt</span>
<?php else: ?>
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kleinplaneten-Favorit wird aktualisiert..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML">
<input type="hidden" name="action" value="toggle_minorplanet_favorite">
<input type="hidden" name="minorplanet_id" value="<?= (int) $minorplanet['id'] ?>">
<button type="submit" class="btn btn-sm btn-secondary<?= (int) $minorplanet['is_favorite'] === 1 ? ' favorite-active' : '' ?>">
<?= (int) $minorplanet['is_favorite'] === 1 ? 'Favorit entfernen' : 'Als Favorit' ?>
</button>
</form>
<?php endif; ?>
<?php if (count($trackedMinorplanets) > 1): ?>
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kleinplanet wird entfernt..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML" onsubmit="return confirm('Kleinplanet wirklich entfernen?');">
<input type="hidden" name="action" value="remove_minorplanet">
<input type="hidden" name="minorplanet_id" value="<?= (int) $minorplanet['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Entfernen</button>
</form>
<?php else: ?>
<span class="hint">Letzter Kleinplanet</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
@@ -470,18 +478,26 @@ function renderTrackedCometsSection(array $trackedComets, bool $oob = false): st
<td class="<?= (int) $comet['is_favorite'] === 1 ? 'favorite-text' : '' ?>"><?= h((string) $comet['orbit_type']) ?></td>
<td class="<?= (int) $comet['is_favorite'] === 1 ? 'favorite-text' : '' ?>"><?= (int) $comet['is_favorite'] === 1 ? '*' : '-' ?></td>
<td class="action-cell">
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kometen-Favorit wird aktualisiert..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML">
<input type="hidden" name="action" value="toggle_comet_favorite">
<input type="hidden" name="comet_id" value="<?= (int) $comet['id'] ?>">
<button type="submit" class="btn btn-sm btn-secondary<?= (int) $comet['is_favorite'] === 1 ? ' favorite-active' : '' ?>">
<?= (int) $comet['is_favorite'] === 1 ? 'Favorit entfernen' : 'Als Favorit' ?>
</button>
</form>
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Komet wird entfernt..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML" onsubmit="return confirm('Komet wirklich entfernen?');">
<input type="hidden" name="action" value="remove_comet">
<input type="hidden" name="comet_id" value="<?= (int) $comet['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Entfernen</button>
</form>
<?php if (count($trackedComets) <= 1 && (int) $comet['is_favorite'] === 1): ?>
<span class="hint">Favorit bleibt gesetzt</span>
<?php else: ?>
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kometen-Favorit wird aktualisiert..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML">
<input type="hidden" name="action" value="toggle_comet_favorite">
<input type="hidden" name="comet_id" value="<?= (int) $comet['id'] ?>">
<button type="submit" class="btn btn-sm btn-secondary<?= (int) $comet['is_favorite'] === 1 ? ' favorite-active' : '' ?>">
<?= (int) $comet['is_favorite'] === 1 ? 'Favorit entfernen' : 'Als Favorit' ?>
</button>
</form>
<?php endif; ?>
<?php if (count($trackedComets) > 1): ?>
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Komet wird entfernt..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML" onsubmit="return confirm('Komet wirklich entfernen?');">
<input type="hidden" name="action" value="remove_comet">
<input type="hidden" name="comet_id" value="<?= (int) $comet['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Entfernen</button>
</form>
<?php else: ?>
<span class="hint">Letzter Komet</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
@@ -566,6 +582,35 @@ function setExclusiveFavorite(PDO $pdo, string $tableName, string $idColumn, int
':entity_id' => $entityId,
]);
if ($newValue === 0) {
$stmtNextFavorite = $pdo->prepare("
SELECT `{$idColumn}`
FROM `{$tableName}`
WHERE `user_id` = :user_id
AND `{$idColumn}` <> :entity_id
ORDER BY `created_at` ASC, `{$idColumn}` ASC
LIMIT 1
");
$stmtNextFavorite->execute([
':user_id' => $userId,
':entity_id' => $entityId,
]);
$nextEntityId = $stmtNextFavorite->fetchColumn();
if ($nextEntityId !== false) {
$stmtPromote = $pdo->prepare("
UPDATE `{$tableName}`
SET `is_favorite` = 1
WHERE `user_id` = :user_id
AND `{$idColumn}` = :entity_id
");
$stmtPromote->execute([
':user_id' => $userId,
':entity_id' => (int) $nextEntityId,
]);
}
}
$pdo->commit();
return true;
} catch (Throwable $e) {
@@ -590,6 +635,51 @@ function countUserRows(PDO $pdo, string $tableName, int $userId): int
return (int) $stmtCount->fetchColumn();
}
function ensureFavoriteIfNeeded(PDO $pdo, string $tableName, string $idColumn, int $userId): void
{
$stmtFavoriteCount = $pdo->prepare("
SELECT COUNT(*)
FROM `{$tableName}`
WHERE `user_id` = :user_id
AND `is_favorite` = 1
");
$stmtFavoriteCount->execute([
':user_id' => $userId,
]);
$favoriteCount = (int) $stmtFavoriteCount->fetchColumn();
if ($favoriteCount > 0) {
return;
}
$stmtNextFavorite = $pdo->prepare("
SELECT `{$idColumn}`
FROM `{$tableName}`
WHERE `user_id` = :user_id
ORDER BY `created_at` ASC, `{$idColumn}` ASC
LIMIT 1
");
$stmtNextFavorite->execute([
':user_id' => $userId,
]);
$nextEntityId = $stmtNextFavorite->fetchColumn();
if ($nextEntityId === false) {
return;
}
$stmtPromote = $pdo->prepare("
UPDATE `{$tableName}`
SET `is_favorite` = 1
WHERE `user_id` = :user_id
AND `{$idColumn}` = :entity_id
");
$stmtPromote->execute([
':user_id' => $userId,
':entity_id' => (int) $nextEntityId,
]);
}
function validateLocationForm(array $input, array &$errors): array
{
$name = trim((string) ($input['name'] ?? ''));
@@ -879,6 +969,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errors[] = 'Ungültiger Kleinplanet.';
}
if (empty($errors)) {
$minorplanetCount = countUserRows($pdo, 'app_user_minorplanets', $userId);
if ($minorplanetCount <= 1) {
$errors[] = 'Der letzte Kleinplanet kann nicht gelöscht werden.';
}
}
if (empty($errors)) {
$stmtDelete = $pdo->prepare("
DELETE FROM `app_user_minorplanets`
@@ -901,6 +998,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errors[] = 'Ungültiger Kleinplanet.';
}
if (empty($errors)) {
$stmtCurrentMinorplanet = $pdo->prepare("
SELECT `is_favorite`
FROM `app_user_minorplanets`
WHERE `user_id` = :user_id
AND `minorplanet_id` = :minorplanet_id
LIMIT 1
");
$stmtCurrentMinorplanet->execute([
':user_id' => $userId,
':minorplanet_id' => $minorplanetId,
]);
$currentMinorplanetFavorite = $stmtCurrentMinorplanet->fetchColumn();
if ($currentMinorplanetFavorite === false) {
$errors[] = 'Der Kleinplanet ist nicht in deiner Liste vorhanden.';
}
}
if (empty($errors)) {
$minorplanetCount = countUserRows($pdo, 'app_user_minorplanets', $userId);
if ($minorplanetCount <= 1 && (int) $currentMinorplanetFavorite === 1) {
$errors[] = 'Beim letzten Kleinplaneten kann der Favorit nicht entfernt werden.';
}
}
if (empty($errors)) {
if (!setExclusiveFavorite($pdo, 'app_user_minorplanets', 'minorplanet_id', $userId, $minorplanetId)) {
$errors[] = 'Der Kleinplanet ist nicht in deiner Liste vorhanden.';
@@ -965,6 +1088,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errors[] = 'Ungueltiger Komet.';
}
if (empty($errors)) {
$cometCount = countUserRows($pdo, 'app_user_comets', $userId);
if ($cometCount <= 1) {
$errors[] = 'Der letzte Komet kann nicht gelöscht werden.';
}
}
if (empty($errors)) {
$stmtDelete = $pdo->prepare("
DELETE FROM `app_user_comets`
@@ -987,6 +1117,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errors[] = 'Ungueltiger Komet.';
}
if (empty($errors)) {
$stmtCurrentComet = $pdo->prepare("
SELECT `is_favorite`
FROM `app_user_comets`
WHERE `user_id` = :user_id
AND `comet_id` = :comet_id
LIMIT 1
");
$stmtCurrentComet->execute([
':user_id' => $userId,
':comet_id' => $cometId,
]);
$currentCometFavorite = $stmtCurrentComet->fetchColumn();
if ($currentCometFavorite === false) {
$errors[] = 'Der Komet ist nicht in deiner Liste vorhanden.';
}
}
if (empty($errors)) {
$cometCount = countUserRows($pdo, 'app_user_comets', $userId);
if ($cometCount <= 1 && (int) $currentCometFavorite === 1) {
$errors[] = 'Beim letzten Kometen kann der Favorit nicht entfernt werden.';
}
}
if (empty($errors)) {
if (!setExclusiveFavorite($pdo, 'app_user_comets', 'comet_id', $userId, $cometId)) {
$errors[] = 'Der Komet ist nicht in deiner Liste vorhanden.';
@@ -1329,6 +1485,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
ensureFavoriteIfNeeded($pdo, 'app_user_minorplanets', 'minorplanet_id', $userId);
ensureFavoriteIfNeeded($pdo, 'app_user_comets', 'comet_id', $userId);
}
$stmtLocations = $pdo->prepare("
SELECT
`id`,
@@ -1987,18 +2148,26 @@ $loggedIn = true;
<td class="<?= (int) $minorplanet['is_favorite'] === 1 ? 'favorite-text' : '' ?>"><?= h($minorplanet['mp_number'] !== null ? (string) $minorplanet['mp_number'] : '-') ?></td>
<td class="<?= (int) $minorplanet['is_favorite'] === 1 ? 'favorite-text' : '' ?>"><?= (int) $minorplanet['is_favorite'] === 1 ? '*' : '-' ?></td>
<td class="action-cell">
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kleinplaneten-Favorit wird aktualisiert..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML">
<input type="hidden" name="action" value="toggle_minorplanet_favorite">
<input type="hidden" name="minorplanet_id" value="<?= (int) $minorplanet['id'] ?>">
<button type="submit" class="btn btn-sm btn-secondary<?= (int) $minorplanet['is_favorite'] === 1 ? ' favorite-active' : '' ?>">
<?= (int) $minorplanet['is_favorite'] === 1 ? 'Favorit entfernen' : 'Als Favorit' ?>
</button>
</form>
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kleinplanet wird entfernt..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML" onsubmit="return confirm('Kleinplanet wirklich entfernen?');">
<input type="hidden" name="action" value="remove_minorplanet">
<input type="hidden" name="minorplanet_id" value="<?= (int) $minorplanet['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Entfernen</button>
</form>
<?php if (count($trackedMinorplanets) <= 1 && (int) $minorplanet['is_favorite'] === 1): ?>
<span class="hint">Favorit bleibt gesetzt</span>
<?php else: ?>
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kleinplaneten-Favorit wird aktualisiert..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML">
<input type="hidden" name="action" value="toggle_minorplanet_favorite">
<input type="hidden" name="minorplanet_id" value="<?= (int) $minorplanet['id'] ?>">
<button type="submit" class="btn btn-sm btn-secondary<?= (int) $minorplanet['is_favorite'] === 1 ? ' favorite-active' : '' ?>">
<?= (int) $minorplanet['is_favorite'] === 1 ? 'Favorit entfernen' : 'Als Favorit' ?>
</button>
</form>
<?php endif; ?>
<?php if (count($trackedMinorplanets) > 1): ?>
<form method="post" action="" class="favorites-loading-form favorites-loading-form-htmx" data-loading-message="Kleinplanet wird entfernt..." hx-post="my_favorites.php" hx-target="#favorites-messages" hx-swap="outerHTML" onsubmit="return confirm('Kleinplanet wirklich entfernen?');">
<input type="hidden" name="action" value="remove_minorplanet">
<input type="hidden" name="minorplanet_id" value="<?= (int) $minorplanet['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Entfernen</button>
</form>
<?php else: ?>
<span class="hint">Letzter Kleinplanet</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
+84
View File
@@ -33,6 +33,8 @@ const DEFAULT_LOCATION_LONGITUDE = 11.189;
const DEFAULT_LOCATION_ELEVATION = 640.0;
const DEFAULT_LOCATION_TIMEZONE = 'Europe/Berlin';
const DEFAULT_SATELLITE_ID = 61;
const DEFAULT_MINORPLANET_NUMBER = 1;
const DEFAULT_COMET_SEARCH = 'Halley';
function sendRegistrationNotification(string $username, string $email, ?string $displayName): void
{
@@ -77,6 +79,39 @@ function textLength(string $value): int
return strlen($value);
}
function findDefaultMinorplanetId(PDO $pdo): ?int
{
$stmt = $pdo->prepare("
SELECT `id`
FROM `minorplanets_mpc`
WHERE `mp_number` = :mp_number
LIMIT 1
");
$stmt->execute([
':mp_number' => DEFAULT_MINORPLANET_NUMBER,
]);
$id = $stmt->fetchColumn();
return $id !== false ? (int) $id : null;
}
function findDefaultCometId(PDO $pdo): ?int
{
$stmt = $pdo->prepare("
SELECT `id`
FROM `comets_mpc`
WHERE `designation_and_name` LIKE :search
ORDER BY `designation_and_name` ASC, `id` ASC
LIMIT 1
");
$stmt->execute([
':search' => '%' . DEFAULT_COMET_SEARCH . '%',
]);
$id = $stmt->fetchColumn();
return $id !== false ? (int) $id : null;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
@@ -131,6 +166,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($errors)) {
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
$defaultMinorplanetId = findDefaultMinorplanetId($pdo);
$defaultCometId = findDefaultCometId($pdo);
if ($defaultMinorplanetId === null) {
$errors[] = 'Der Standard-Kleinplanet Ceres wurde in der Datenbank nicht gefunden.';
}
if ($defaultCometId === null) {
$errors[] = 'Der Standard-Komet Halley wurde in der Datenbank nicht gefunden.';
}
}
if (empty($errors)) {
$pdo->beginTransaction();
try {
@@ -215,6 +263,42 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
':satellite_id' => DEFAULT_SATELLITE_ID,
]);
$stmtInsertMinorplanet = $pdo->prepare("
INSERT INTO `app_user_minorplanets` (
`user_id`,
`minorplanet_id`,
`is_favorite`,
`created_at`
) VALUES (
:user_id,
:minorplanet_id,
1,
NOW()
)
");
$stmtInsertMinorplanet->execute([
':user_id' => $userId,
':minorplanet_id' => $defaultMinorplanetId,
]);
$stmtInsertComet = $pdo->prepare("
INSERT INTO `app_user_comets` (
`user_id`,
`comet_id`,
`is_favorite`,
`created_at`
) VALUES (
:user_id,
:comet_id,
1,
NOW()
)
");
$stmtInsertComet->execute([
':user_id' => $userId,
':comet_id' => $defaultCometId,
]);
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {