Initial CAT application and deployment update

This commit is contained in:
2026-09-19 23:27:40 +00:00
commit 6684062400
22 changed files with 3690 additions and 0 deletions
+731
View File
@@ -0,0 +1,731 @@
<?php
declare(strict_types=1);
session_set_cookie_params([
'httponly' => true,
'samesite' => 'Strict',
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
'use_strict_mode' => true,
]);
session_start();
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: no-referrer');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'");
require_once __DIR__ . '/inc/db.inc.php';
require_once __DIR__ . '/inc/mail.inc.php';
function postString(string $key): string
{
$value = $_POST[$key] ?? '';
return is_string($value) ? trim($value) : '';
}
function postStringList(string $key): array
{
$value = $_POST[$key] ?? [];
$values = is_array($value) ? $value : [$value];
return array_values(array_filter(
array_map(
static fn ($item): string => is_string($item) ? trim($item) : '',
$values
),
static fn (string $item): bool => $item !== ''
));
}
function postMealSelectionsByDay(): array
{
$value = $_POST['meals'] ?? [];
if (!is_array($value)) {
return [];
}
$result = [];
foreach ($value as $day => $meals) {
if (!is_array($meals)) {
continue;
}
$result[(string) $day] = array_values(array_filter(
array_map(static fn ($item): string => is_string($item) ? trim($item) : '', $meals),
static fn (string $item): bool => $item !== ''
));
}
return $result;
}
function escapeHtml(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function earlyDepartureLabel(?string $value): string
{
return match ($value) {
'sat_breakfast' => 'Samstag nach dem Frühstück',
'sat_lunch' => 'Samstag nach dem Mittagessen',
'sat_coffee' => 'Samstag nach dem Kaffee',
'sat_dinner' => 'Samstag nach dem Abendessen',
'sun_breakfast' => 'Sonntag nach dem Frühstück',
null, '' => 'Keine vorzeitige Abreise',
default => $value,
};
}
function calculateBookingTotal(PDO $pdo, bool $under27, string $guestType, ?string $roomType, array $mealsByDay): float
{
$ageSuffix = $under27 ? 'under_27' : 'over_27';
$keys = [];
if ($guestType === 'regular_guest' && $roomType !== null) {
$keys[] = $roomType . '_' . $ageSuffix;
}
if ($guestType === 'day_guest') {
foreach ($mealsByDay as $meals) {
foreach (array_unique(array_merge($meals, ['drinks'])) as $meal) {
if (in_array($meal, ['breakfast', 'lunch', 'coffee', 'dinner'], true)) {
$keys[] = $meal . '_' . $ageSuffix;
} elseif ($meal === 'drinks') {
$keys[] = 'drinks';
}
}
$keys[] = 'rehearsal_room';
}
}
if ($keys === []) {
return 0.0;
}
$uniqueKeys = array_values(array_unique($keys));
$placeholders = implode(',', array_fill(0, count($uniqueKeys), '?'));
$statement = $pdo->prepare("SELECT setting_key, price FROM pricing_settings WHERE setting_key IN ($placeholders)");
$statement->execute($uniqueKeys);
$prices = [];
foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) {
$prices[(string) $row['setting_key']] = (float) $row['price'];
}
return round(array_sum(array_map(
static fn (string $key): float => $prices[$key] ?? 0.0,
$keys
)), 2);
}
function createPdo(array $dbConfig): PDO
{
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$dbConfig['host'],
$dbConfig['port'],
$dbConfig['database'],
$dbConfig['charset']
);
return new PDO($dsn, $dbConfig['username'], $dbConfig['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
function editSmtpResponse($socket): string
{
$response = '';
do {
$line = fgets($socket);
if ($line === false) {
throw new RuntimeException('Keine gültige SMTP-Antwort erhalten.');
}
$response .= $line;
} while (isset($line[3]) && $line[3] === '-');
return $response;
}
function editSmtpCommand($socket, string $command, array $expectedCodes): void
{
fwrite($socket, $command . "\r\n");
$response = editSmtpResponse($socket);
if (!in_array((int) substr($response, 0, 3), $expectedCodes, true)) {
throw new RuntimeException('SMTP-Befehl wurde abgelehnt.');
}
}
function editEncodedHeader(string $value): string
{
return '=?UTF-8?B?' . base64_encode($value) . '?=';
}
function sendDeletionAdminEmail(array $mailConfig, string $recipient, array $booking): void
{
$message = 'From: ' . editEncodedHeader($mailConfig['from_name']) . ' <' . $mailConfig['from_address'] . ">\r\n"
. 'To: ' . $recipient . "\r\n"
. 'Subject: ' . editEncodedHeader('Registrierung gelöscht Chorwochenende') . "\r\n"
. "MIME-Version: 1.0\r\n"
. "Content-Type: text/plain; charset=UTF-8\r\n"
. "Content-Transfer-Encoding: 8bit\r\n\r\n"
. "Eine Teilnehmerregistrierung wurde selbstständig gelöscht.\n\n"
. "Name: {$booking['name']}\n"
. "E-Mail: {$booking['email']}\n"
. "Gasttyp: {$booking['role']}\n"
. "Zimmerart: {$booking['accommodation_type']}\n"
. "Zimmerpartner-Wunsch: {$booking['roommate_requests']}\n"
. "Sonstiges: {$booking['other_notes']}\n";
$context = stream_context_create([
'ssl' => [
'peer_name' => $mailConfig['host'],
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$socket = stream_socket_client(
'tcp://' . $mailConfig['host'] . ':' . $mailConfig['port'],
$errorNumber,
$errorMessage,
20,
STREAM_CLIENT_CONNECT,
$context
);
if ($socket === false) {
throw new RuntimeException('SMTP-Server nicht erreichbar.');
}
stream_set_timeout($socket, 20);
try {
if ((int) substr(editSmtpResponse($socket), 0, 3) !== 220) {
throw new RuntimeException('SMTP-Verbindung wurde abgelehnt.');
}
editSmtpCommand($socket, 'EHLO localhost', [250]);
editSmtpCommand($socket, 'STARTTLS', [220]);
if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
throw new RuntimeException('STARTTLS konnte nicht aktiviert werden.');
}
editSmtpCommand($socket, 'EHLO localhost', [250]);
editSmtpCommand($socket, 'AUTH LOGIN', [334]);
editSmtpCommand($socket, base64_encode($mailConfig['username']), [334]);
editSmtpCommand($socket, base64_encode($mailConfig['password']), [235]);
editSmtpCommand($socket, 'MAIL FROM:<' . $mailConfig['from_address'] . '>', [250]);
editSmtpCommand($socket, 'RCPT TO:<' . $recipient . '>', [250, 251]);
editSmtpCommand($socket, 'DATA', [354]);
fwrite($socket, preg_replace('/^\./m', '..', $message) . "\r\n.\r\n");
if ((int) substr(editSmtpResponse($socket), 0, 3) !== 250) {
throw new RuntimeException('E-Mail konnte nicht angenommen werden.');
}
fwrite($socket, "QUIT\r\n");
} finally {
fclose($socket);
}
}
function redirectToEdit(): never
{
header('Location: edit_registration.php');
exit;
}
function editCsrfToken(): string
{
if (!isset($_SESSION['edit_csrf'])) {
$_SESSION['edit_csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['edit_csrf'];
}
function requireEditCsrf(): void
{
$submitted = postString('csrf_token');
$stored = $_SESSION['edit_csrf'] ?? '';
if ($stored === '' || !hash_equals($stored, $submitted)) {
http_response_code(403);
exit('Ungültige Anfrage.');
}
}
function validateBookingInput(): array
{
$data = [
'name' => postString('name'),
'email' => postString('email'),
'guest_type' => postString('guest_type'),
'room_type' => postString('room_type'),
'roommates' => postString('roommates'),
'early_departure' => postString('early_departure'),
'allergies' => postString('allergies'),
'other_notes' => postString('other_notes'),
'days' => postStringList('days'),
'meals_by_day' => postMealSelectionsByDay(),
];
$errors = [];
if ($data['name'] === '' || mb_strlen($data['name']) > 255) {
$errors[] = 'Bitte einen gültigen Namen eingeben.';
}
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL) || mb_strlen($data['email']) > 255) {
$errors[] = 'Bitte eine gültige E-Mail-Adresse eingeben.';
}
if (!in_array($data['guest_type'], ['day_guest', 'regular_guest'], true)) {
$errors[] = 'Bitte einen gültigen Gasttyp auswählen.';
}
if ($data['guest_type'] === 'regular_guest'
&& !in_array($data['room_type'], ['single', 'double', 'multi'], true)) {
$errors[] = 'Bitte eine gültige Zimmerart auswählen.';
}
$allowedEarlyDepartures = ['sat_breakfast', 'sat_lunch', 'sat_coffee', 'sat_dinner', 'sun_breakfast'];
if ($data['early_departure'] !== ''
&& ($data['guest_type'] !== 'regular_guest' || !in_array($data['early_departure'], $allowedEarlyDepartures, true))) {
$errors[] = 'Bitte einen gültigen Abreisezeitpunkt auswählen.';
}
if (mb_strlen($data['other_notes']) > 5000) {
$errors[] = 'Das Feld Sonstiges darf höchstens 5000 Zeichen enthalten.';
}
if (mb_strlen($data['roommates']) > 1000 || mb_strlen($data['allergies']) > 1000) {
$errors[] = 'Zimmerwunsch und Allergieangaben dürfen höchstens 1000 Zeichen enthalten.';
}
$allowedDays = ['1', '2', '3'];
$allowedMealsByDay = [
'1' => ['dinner'],
'2' => ['breakfast', 'lunch', 'coffee', 'dinner'],
'3' => ['breakfast', 'lunch'],
];
$data['days'] = array_values(array_unique($data['days']));
foreach ($data['meals_by_day'] as $day => $dayMeals) {
$data['meals_by_day'][$day] = array_values(array_unique($dayMeals));
}
if (count($data['days']) > count($allowedDays) || array_diff($data['days'], $allowedDays) !== []) {
$errors[] = 'Bitte nur gültige Anwesenheitstage auswählen.';
}
if (array_diff(array_keys($data['meals_by_day']), $allowedDays) !== []) {
$errors[] = 'Bitte nur gültige Mahlzeitentage auswählen.';
}
foreach ($data['meals_by_day'] as $day => $dayMeals) {
$allowedMeals = $allowedMealsByDay[(string) $day] ?? [];
if (array_diff($dayMeals, $allowedMeals) !== []) {
$errors[] = 'Bitte nur gültige Mahlzeiten auswählen.';
}
if (!in_array((string) $day, $data['days'], true) && $dayMeals !== []) {
$errors[] = 'Mahlzeiten dürfen nur für ausgewählte Anwesenheitstage angegeben werden.';
}
}
foreach ($data['days'] as $day) {
$data['meals_by_day'][$day] ??= [];
}
if ($errors !== []) {
throw new InvalidArgumentException(implode(' ', $errors));
}
return $data;
}
$pdo = createPdo($dbConfig);
$message = $_SESSION['edit_message'] ?? '';
unset($_SESSION['edit_message']);
if (postString('action') === 'login') {
$email = postString('email');
$password = postString('password');
$lockedUntil = (int) ($_SESSION['edit_locked_until'] ?? 0);
if ($lockedUntil > time()) {
$message = 'Zu viele Fehlversuche. Bitte später erneut versuchen.';
} else {
$statement = $pdo->prepare(
'SELECT p.id AS participant_id, b.id AS booking_id, p.password_hash
FROM participants p
INNER JOIN bookings b ON b.participant_id = p.id
WHERE p.email = :email
LIMIT 1'
);
$statement->execute([':email' => $email]);
$participant = $statement->fetch(PDO::FETCH_ASSOC);
if ($participant !== false && password_verify($password, $participant['password_hash'])) {
unset($_SESSION['edit_login_failures'], $_SESSION['edit_locked_until']);
session_regenerate_id(true);
$_SESSION['edit_participant_id'] = $participant['participant_id'];
$_SESSION['edit_booking_id'] = (int) $participant['booking_id'];
$_SESSION['edit_csrf'] = bin2hex(random_bytes(32));
$_SESSION['edit_last_activity'] = time();
redirectToEdit();
}
$failures = (int) ($_SESSION['edit_login_failures'] ?? 0) + 1;
$_SESSION['edit_login_failures'] = $failures;
if ($failures >= 5) {
$_SESSION['edit_locked_until'] = time() + 300;
}
usleep(500000);
$message = 'E-Mail-Adresse oder Passwort ist ungültig.';
}
}
if (isset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'])) {
$lastActivity = (int) ($_SESSION['edit_last_activity'] ?? 0);
if ($lastActivity > 0 && $lastActivity < time() - 1800) {
unset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'], $_SESSION['edit_csrf']);
session_regenerate_id(true);
} else {
$_SESSION['edit_last_activity'] = time();
}
}
if (!isset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'])) {
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registrierung ändern</title>
<link rel="stylesheet" href="./css/booking.css">
</head>
<body>
<main class="success-card">
<h1>Registrierung ändern</h1>
<p>Gib die E-Mail-Adresse und das Passwort ein, die du nach der Registrierung erhalten hast.</p>
<?php if ($message !== ''): ?>
<p class="message error"><?= escapeHtml($message) ?></p>
<?php endif; ?>
<form method="post" autocomplete="off">
<input type="hidden" name="action" value="login">
<div class="form-group">
<label for="email">E-Mail-Adresse:</label>
<input type="email" id="email" name="email" required autocomplete="username">
<label for="password">Passwort:</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit">Registrierung laden</button>
</form>
<p><a href="index.php">Zur Startseite</a></p>
</main>
</body>
</html><?php
exit;
}
$participantId = (string) $_SESSION['edit_participant_id'];
$bookingId = (int) $_SESSION['edit_booking_id'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
requireEditCsrf();
$action = postString('action');
try {
if ($action === 'logout') {
unset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'], $_SESSION['edit_csrf']);
setcookie(session_name(), '', [
'expires' => time() - 42000,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
'httponly' => true,
'samesite' => 'Strict',
]);
session_destroy();
redirectToEdit();
}
if ($action === 'delete_booking') {
$deletedStatement = $pdo->prepare(
'SELECT p.name, p.email, p.role, b.accommodation_type,
b.roommate_requests, b.early_departure, b.other_notes
FROM participants p
INNER JOIN bookings b ON b.participant_id = p.id
WHERE p.id = :participant_id AND b.id = :booking_id'
);
$deletedStatement->execute([
':participant_id' => $participantId,
':booking_id' => $bookingId,
]);
$deletedBooking = $deletedStatement->fetch(PDO::FETCH_ASSOC);
if ($deletedBooking === false) {
throw new InvalidArgumentException('Registrierung nicht gefunden.');
}
$deletedBooking['accommodation_type'] = match ($deletedBooking['accommodation_type']) {
'single' => 'Einzelzimmer',
'double' => 'Doppelzimmer',
'multi' => 'Mehrbettzimmer',
default => '',
};
$deletedBooking['roommate_requests'] = $deletedBooking['roommate_requests'] ?: '';
$deletedBooking['other_notes'] = $deletedBooking['other_notes'] ?: '';
$pdo->beginTransaction();
$deleteBooking = $pdo->prepare(
'DELETE FROM bookings WHERE id = :booking_id AND participant_id = :participant_id'
);
$deleteBooking->execute([
':booking_id' => $bookingId,
':participant_id' => $participantId,
]);
$deleteParticipant = $pdo->prepare('DELETE FROM participants WHERE id = :participant_id');
$deleteParticipant->execute([':participant_id' => $participantId]);
$pdo->commit();
$adminEmails = $pdo->query(
"SELECT email FROM admin_users WHERE is_active = 1 AND email <> ''"
)->fetchAll(PDO::FETCH_COLUMN);
foreach ($adminEmails as $adminEmail) {
try {
sendDeletionAdminEmail($mailConfig, (string) $adminEmail, $deletedBooking);
} catch (Throwable $exception) {
error_log('Admin-Benachrichtigung über Löschung konnte nicht versendet werden: ' . $exception->getMessage());
}
}
unset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'], $_SESSION['edit_csrf']);
$_SESSION['edit_message'] = 'Die Registrierung wurde gelöscht.';
redirectToEdit();
}
if ($action === 'update_booking') {
$data = validateBookingInput();
$role = $data['guest_type'] === 'day_guest' ? 'Tagesgast' : 'Dauergast';
$bookingType = $data['guest_type'] === 'day_guest' ? 'Day' : 'Stay';
$accommodationType = $data['guest_type'] === 'regular_guest' ? $data['room_type'] : null;
$roommateRequest = $data['guest_type'] === 'regular_guest' && $data['roommates'] !== ''
? $data['roommates']
: null;
$allowedEarlyDepartures = ['sat_breakfast', 'sat_lunch', 'sat_coffee', 'sat_dinner', 'sun_breakfast'];
$earlyDeparture = $data['guest_type'] === 'regular_guest'
&& in_array($data['early_departure'], $allowedEarlyDepartures, true)
? $data['early_departure']
: null;
$mealPreferences = $data['guest_type'] === 'day_guest'
? json_encode(['days' => $data['days'], 'meals_by_day' => $data['meals_by_day']], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)
: null;
$pdo->beginTransaction();
$existingPriceStatement = $pdo->prepare(
'SELECT manual_total_price FROM bookings WHERE id = :booking_id AND participant_id = :participant_id FOR UPDATE'
);
$existingPriceStatement->execute([
':booking_id' => $bookingId,
':participant_id' => $participantId,
]);
$existingManualPrice = $existingPriceStatement->fetchColumn();
if ($existingManualPrice === false) {
throw new InvalidArgumentException('Registrierung nicht gefunden.');
}
$participantStatement = $pdo->prepare(
'UPDATE participants
SET name = :name, email = :email, allergies = :allergies, role = :role, under_27 = :under_27
WHERE id = :participant_id'
);
$participantStatement->execute([
':name' => $data['name'],
':email' => $data['email'],
':allergies' => $data['allergies'] !== '' ? $data['allergies'] : null,
':role' => $role,
':under_27' => isset($_POST['under_27']) && $_POST['under_27'] === '1' ? 1 : 0,
':participant_id' => $participantId,
]);
$totalPrice = calculateBookingTotal(
$pdo,
isset($_POST['under_27']) && $_POST['under_27'] === '1',
$data['guest_type'],
$accommodationType,
$data['meals_by_day']
);
$bookingStatement = $pdo->prepare(
'UPDATE bookings
SET type = :type, accommodation_type = :accommodation_type,
meal_preferences = :meal_preferences, roommate_requests = :roommate_requests,
early_departure = :early_departure,
other_notes = :other_notes, total_price = :total_price
WHERE id = :booking_id AND participant_id = :participant_id'
);
$bookingStatement->execute([
':type' => $bookingType,
':accommodation_type' => $accommodationType,
':meal_preferences' => $mealPreferences,
':roommate_requests' => $roommateRequest,
':early_departure' => $earlyDeparture,
':other_notes' => $data['other_notes'] !== '' ? $data['other_notes'] : null,
':total_price' => $existingManualPrice !== null
? number_format((float) $existingManualPrice, 2, '.', '')
: number_format($totalPrice, 2, '.', ''),
':booking_id' => $bookingId,
':participant_id' => $participantId,
]);
$pdo->commit();
$_SESSION['edit_message'] = 'Die Registrierung wurde aktualisiert.';
redirectToEdit();
}
} catch (InvalidArgumentException | JsonException $exception) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$message = $exception->getMessage();
} catch (PDOException $exception) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log($exception->getMessage());
$message = 'Die Änderung konnte nicht gespeichert werden. Prüfe bitte deine Eingaben.';
}
}
$statement = $pdo->prepare(
'SELECT p.name, p.email, p.role, p.under_27, p.allergies,
b.type, b.accommodation_type, b.meal_preferences,
b.roommate_requests, b.early_departure, b.other_notes
FROM participants p
INNER JOIN bookings b ON b.participant_id = p.id
WHERE p.id = :participant_id AND b.id = :booking_id'
);
$statement->execute([':participant_id' => $participantId, ':booking_id' => $bookingId]);
$booking = $statement->fetch(PDO::FETCH_ASSOC);
if ($booking === false) {
unset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'], $_SESSION['edit_csrf']);
http_response_code(404);
exit('Registrierung nicht gefunden.');
}
$mealData = $booking['meal_preferences'] !== null
? json_decode($booking['meal_preferences'], true)
: [];
$mealData = is_array($mealData) ? $mealData : [];
$selectedMealsByDay = is_array($mealData['meals_by_day'] ?? null) ? $mealData['meals_by_day'] : [];
if ($selectedMealsByDay === [] && is_array($mealData['meals'] ?? null)) {
foreach ($mealData['days'] ?? [] as $day) {
$selectedMealsByDay[(string) $day] = $mealData['meals'];
}
}
$guestType = $booking['type'] === 'Day' ? 'day_guest' : 'regular_guest';
$csrf = editCsrfToken();
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registrierung ändern</title>
<link rel="stylesheet" href="./css/booking.css">
</head>
<body>
<main class="success-card">
<h1>Registrierung ändern</h1>
<?php if ($message !== ''): ?>
<p class="message"><?= escapeHtml($message) ?></p>
<?php endif; ?>
<form method="post">
<input type="hidden" name="action" value="update_booking">
<input type="hidden" name="csrf_token" value="<?= escapeHtml($csrf) ?>">
<div class="form-group">
<label for="name">Name des Gastes:</label>
<input type="text" id="name" name="name" maxlength="255" value="<?= escapeHtml($booking['name']) ?>" required>
<label for="email">E-Mail-Adresse:</label>
<input type="email" id="email" name="email" maxlength="255" value="<?= escapeHtml($booking['email']) ?>" required>
<label class="checkbox-label" for="under_27">
<input type="checkbox" id="under_27" name="under_27" value="1" <?= (int) $booking['under_27'] === 1 ? 'checked' : '' ?>>
Unter 27
</label>
</div>
<div class="form-group">
<label for="guest_type">Gasttyp:</label>
<select id="guest_type" name="guest_type" required>
<option value="">Bitte auswählen</option>
<option value="day_guest" <?= $guestType === 'day_guest' ? 'selected' : '' ?>>Tagesgast</option>
<option value="regular_guest" <?= $guestType === 'regular_guest' ? 'selected' : '' ?>>Dauergast</option>
</select>
</div>
<div class="form-group" id="room_details">
<div id="regular_guest_options">
<label for="room_type">Zimmerart:</label>
<select id="room_type" name="room_type">
<option value="single" <?= $booking['accommodation_type'] === 'single' ? 'selected' : '' ?>>Einzelzimmer</option>
<option value="double" <?= $booking['accommodation_type'] === 'double' ? 'selected' : '' ?>>Doppelzimmer</option>
<option value="multi" <?= $booking['accommodation_type'] === 'multi' ? 'selected' : '' ?>>Mehrbettzimmer</option>
</select>
<div id="roommate_info" style="display: none;">
<label for="roommates">Mit wem möchtest du das Zimmer teilen?</label>
<input type="text" id="roommates" name="roommates" value="<?= escapeHtml((string) ($booking['roommate_requests'] ?? '')) ?>" placeholder="Name des Mitbewohners">
</div>
<label class="checkbox-label" for="early_departure_enabled">
<input type="checkbox" id="early_departure_enabled" name="early_departure_enabled" value="1" <?= !empty($booking['early_departure']) ? 'checked' : '' ?>>
Ich reise vorzeitig ab.
</label>
<div id="early_departure_options" style="display: none;">
<label>Abreisezeitpunkt:</label>
<?php $earlyDepartureOptions = [
'sat_breakfast' => 'Samstag nach dem Frühstück',
'sat_lunch' => 'Samstag nach dem Mittagessen',
'sat_coffee' => 'Samstag nach dem Kaffee',
'sat_dinner' => 'Samstag nach dem Abendessen',
'sun_breakfast' => 'Sonntag nach dem Frühstück',
]; ?>
<div class="checkbox-group">
<?php foreach ($earlyDepartureOptions as $value => $label): ?>
<label><input type="radio" name="early_departure" value="<?= $value ?>" <?= $booking['early_departure'] === $value ? 'checked' : '' ?>> <?= $label ?></label>
<?php endforeach; ?>
</div>
</div>
</div>
<div id="day_guest_options" style="display: none;">
<label>Anwesenheitstage:</label>
<div class="checkbox-group">
<?php foreach (['1' => 'Fr.', '2' => 'Sa.', '3' => 'So.'] as $value => $label): ?>
<label><input type="checkbox" name="days[]" value="<?= $value ?>" <?= in_array($value, $mealData['days'] ?? [], true) ? 'checked' : '' ?>> <?= $label ?></label>
<?php endforeach; ?>
</div>
<label>Gewünschte Mahlzeiten je Anwesenheitstag:</label>
<?php $mealOptionsByDay = [
'1' => ['dinner' => 'Abendessen'],
'2' => ['breakfast' => 'Frühstück', 'lunch' => 'Mittagessen', 'coffee' => 'Kaffee', 'dinner' => 'Abendessen'],
'3' => ['breakfast' => 'Frühstück', 'lunch' => 'Mittagessen'],
]; ?>
<?php foreach (['1' => 'Fr.', '2' => 'Sa.', '3' => 'So.'] as $dayValue => $dayLabel): ?>
<fieldset class="meal-day">
<legend><?= escapeHtml($dayLabel) ?></legend>
<div class="checkbox-group">
<?php foreach ($mealOptionsByDay[$dayValue] as $value => $label): ?>
<label><input type="checkbox" name="meals[<?= $dayValue ?>][]" value="<?= $value ?>" <?= in_array($value, $selectedMealsByDay[$dayValue] ?? [], true) ? 'checked' : '' ?>> <?= $label ?></label>
<?php endforeach; ?>
</div>
</fieldset>
<?php endforeach; ?>
<p class="form-help">Die Getränkepauschale und die Proberaumpauschale werden für Tagesgäste automatisch pro Anwesenheitstag berechnet.</p>
</div>
</div>
<div class="form-group">
<label for="allergies">Allergien, Essensvorlieben oder religiöse Einschränkungen:</label>
<input type="text" id="allergies" name="allergies" value="<?= escapeHtml((string) ($booking['allergies'] ?? '')) ?>" placeholder="z.B. Vegetarier, Vegan, Glutenunverträglichkeit, etc.">
</div>
<div class="form-group">
<label for="other_notes">Sonstiges:</label>
<textarea id="other_notes" name="other_notes" rows="5" maxlength="5000" placeholder="Weitere Hinweise oder Wünsche"><?= escapeHtml((string) ($booking['other_notes'] ?? '')) ?></textarea>
</div>
<button type="submit">Änderungen speichern</button>
</form>
<form method="post" data-confirm="Möchtest du die Buchung wirklich löschen?" style="margin-top: 16px;">
<input type="hidden" name="action" value="delete_booking">
<input type="hidden" name="csrf_token" value="<?= escapeHtml($csrf) ?>">
<button class="button-danger" type="submit">Gesamte Buchung löschen</button>
</form>
<p><a href="index.php">Zur Startseite</a></p>
</main>
<script src="./scripts/functions.js?v=20260918-2"></script>
<script src="./scripts/security.js"></script>
</body>
</html>