732 lines
31 KiB
PHP
732 lines
31 KiB
PHP
<?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>
|