Buchungsbereich als Komponente umgebaut und Bestätigungsseite ergänzt #3
|
|
@ -0,0 +1,169 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<title>Buchungsbestätigung – Tri-Hub</title>
|
||||||
|
</head>
|
||||||
|
<body class="confirmation">
|
||||||
|
<header class="confirmation__header">
|
||||||
|
<a
|
||||||
|
class="confirmation__brand"
|
||||||
|
href="/index.html"
|
||||||
|
aria-label="Tri-Hub Startseite"
|
||||||
|
><span class="confirmation__mark" aria-hidden="true">TH</span
|
||||||
|
><span
|
||||||
|
>TRI-HUB<small>Ernährung. Ausdauer. Dein Weg.</small></span
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
<a class="confirmation__link" href="/index.html"
|
||||||
|
>Startseite <span aria-hidden="true">↗</span></a
|
||||||
|
>
|
||||||
|
</header>
|
||||||
|
<main class="confirmation__main">
|
||||||
|
<section
|
||||||
|
id="confirmation-empty"
|
||||||
|
class="confirmation__empty"
|
||||||
|
aria-labelledby="empty-title"
|
||||||
|
>
|
||||||
|
<p class="confirmation__eyebrow">Deine Buchungsübersicht</p>
|
||||||
|
<h1 id="empty-title">Keine Bestätigung verfügbar.</h1>
|
||||||
|
<p>
|
||||||
|
In diesem Tab liegt keine gespeicherte Buchungsbestätigung
|
||||||
|
vor. Öffne die Bestätigung im Tab deiner Buchung. Falls du
|
||||||
|
bereits gebucht hast, prüfe deine E-Mails, bevor du erneut
|
||||||
|
buchst.
|
||||||
|
</p>
|
||||||
|
<a class="confirmation__button" href="/angebotsuebersicht.html"
|
||||||
|
>Zur Paketübersicht →</a
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
<div id="confirmation-content" hidden>
|
||||||
|
<section
|
||||||
|
class="confirmation__hero"
|
||||||
|
aria-labelledby="confirmation-title"
|
||||||
|
>
|
||||||
|
<p class="confirmation__eyebrow">
|
||||||
|
<span aria-hidden="true">✓</span> Deine
|
||||||
|
Buchungsbestätigung
|
||||||
|
</p>
|
||||||
|
<h1 id="confirmation-title">
|
||||||
|
Dein nächster Schritt.<br /><em>Alles im Blick.</em>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
Danke für dein Vertrauen. Deine Buchungsdaten wurden
|
||||||
|
gespeichert. Hier findest du deine persönliche
|
||||||
|
Übersicht.
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="confirmation__simulation"
|
||||||
|
id="confirmation-payment"
|
||||||
|
></p>
|
||||||
|
</section>
|
||||||
|
<div class="confirmation__grid">
|
||||||
|
<section
|
||||||
|
class="confirmation__card"
|
||||||
|
aria-labelledby="summary-title"
|
||||||
|
>
|
||||||
|
<p class="confirmation__eyebrow">
|
||||||
|
Dein gewähltes Paket
|
||||||
|
</p>
|
||||||
|
<h2 id="summary-title"></h2>
|
||||||
|
<p
|
||||||
|
id="confirmation-summary"
|
||||||
|
class="confirmation__muted"
|
||||||
|
></p>
|
||||||
|
<dl
|
||||||
|
id="confirmation-totals"
|
||||||
|
class="confirmation__details"
|
||||||
|
></dl>
|
||||||
|
<p id="confirmation-no-snapshot" hidden>
|
||||||
|
Für diese ältere Buchung ist keine Preis- und
|
||||||
|
Leistungsübersicht gespeichert.
|
||||||
|
</p>
|
||||||
|
<div id="confirmation-features-section" hidden>
|
||||||
|
<h3>Das ist enthalten</h3>
|
||||||
|
<ul
|
||||||
|
id="confirmation-features"
|
||||||
|
class="confirmation__features"
|
||||||
|
></ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<aside
|
||||||
|
class="confirmation__sidebar"
|
||||||
|
aria-label="Buchungsdaten und Bestätigungsversand"
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
class="confirmation__card"
|
||||||
|
aria-labelledby="receipt-title"
|
||||||
|
>
|
||||||
|
<p class="confirmation__eyebrow">
|
||||||
|
Für deine Unterlagen
|
||||||
|
</p>
|
||||||
|
<h2 id="receipt-title">Deine Buchung</h2>
|
||||||
|
<dl
|
||||||
|
id="confirmation-receipt"
|
||||||
|
class="confirmation__details confirmation__details--stacked"
|
||||||
|
></dl>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
class="confirmation__card confirmation__mail"
|
||||||
|
aria-labelledby="mail-title"
|
||||||
|
>
|
||||||
|
<h2 id="mail-title">Deine E-Mail-Bestätigung</h2>
|
||||||
|
<p id="confirmation-mail-status"></p>
|
||||||
|
<a
|
||||||
|
id="confirmation-retry"
|
||||||
|
class="confirmation__link"
|
||||||
|
hidden
|
||||||
|
>Versandstatus prüfen →</a
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
<section
|
||||||
|
id="confirmation-steps-section"
|
||||||
|
class="confirmation__card confirmation__next"
|
||||||
|
aria-labelledby="steps-title"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<p class="confirmation__eyebrow">Dein weiterer Weg</p>
|
||||||
|
<h2 id="steps-title">So geht es weiter</h2>
|
||||||
|
<p class="confirmation__muted">
|
||||||
|
So ist der Ablauf deines gewählten Pakets vorgesehen.
|
||||||
|
Die aktuelle Buchung ist eine Simulation.
|
||||||
|
</p>
|
||||||
|
<ol
|
||||||
|
id="confirmation-steps"
|
||||||
|
class="confirmation__steps"
|
||||||
|
></ol>
|
||||||
|
</section>
|
||||||
|
<nav
|
||||||
|
class="confirmation__actions"
|
||||||
|
aria-label="Weiterführende Links"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="confirmation__button"
|
||||||
|
href="/angebotsuebersicht.html"
|
||||||
|
>Zur Paketübersicht <span aria-hidden="true">→</span></a
|
||||||
|
>
|
||||||
|
<a class="confirmation__link" href="/index.html"
|
||||||
|
>Zur Startseite</a
|
||||||
|
>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<noscript
|
||||||
|
>Bitte aktiviere JavaScript, um die gespeicherte
|
||||||
|
Buchungsbestätigung anzuzeigen.</noscript
|
||||||
|
>
|
||||||
|
</main>
|
||||||
|
<footer class="confirmation__footer">
|
||||||
|
TRI-HUB <span>Dein Weg. Deine Ausdauer. Deine Ernährung.</span>
|
||||||
|
</footer>
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="/src/features/booking/booking-confirmation.js"
|
||||||
|
></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -7,7 +7,12 @@
|
||||||
<title>Paket buchen – Tri-Hub</title>
|
<title>Paket buchen – Tri-Hub</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="booking-root" class="container"></main>
|
<main class="container">
|
||||||
|
<a href="/angebotsuebersicht.html">Zur Angebotsseite</a>
|
||||||
|
<h1>Deine Buchung</h1>
|
||||||
|
<!-- Nur diesen Mount-Punkt später in die Paketdetailseite übernehmen. -->
|
||||||
|
<div id="booking-root"></div>
|
||||||
|
</main>
|
||||||
<script
|
<script
|
||||||
type="module"
|
type="module"
|
||||||
src="/src/features/booking/booking-entry.js"
|
src="/src/features/booking/booking-entry.js"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
.confirmation {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #f8fafc;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at 5% 20%, #10b98114, transparent 45%),
|
||||||
|
radial-gradient(ellipse at 95% 45%, #1a1a2e, transparent 55%), #0d0d1a;
|
||||||
|
font-family: Inter, system-ui, sans-serif;
|
||||||
|
line-height: 1.65;
|
||||||
|
color-scheme: dark;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.confirmation * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.confirmation [hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.confirmation a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.confirmation a:focus-visible {
|
||||||
|
outline: 3px solid #10b981;
|
||||||
|
outline-offset: 6px;
|
||||||
|
}
|
||||||
|
.confirmation__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.2rem clamp(1rem, 5vw, 5rem);
|
||||||
|
border-bottom: 1px solid #ffffff12;
|
||||||
|
background: #09091299;
|
||||||
|
}
|
||||||
|
.confirmation__brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.confirmation__brand small {
|
||||||
|
display: block;
|
||||||
|
color: #94a3b8;
|
||||||
|
letter-spacing: normal;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.confirmation__mark {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #087452;
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
letter-spacing: -0.08em;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
.confirmation__main {
|
||||||
|
max-width: 74rem;
|
||||||
|
padding: 4rem 1.5rem;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.confirmation__hero {
|
||||||
|
max-width: 49rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
.confirmation__eyebrow {
|
||||||
|
color: #94a3b8;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.17em;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.confirmation__eyebrow span {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
border: 1px solid #10b98170;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
.confirmation h1 {
|
||||||
|
margin: 1rem 0 1.5rem;
|
||||||
|
font-family: "Playfair Display", Georgia, serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: clamp(2.5rem, 5vw, 4.5rem);
|
||||||
|
line-height: 1.12;
|
||||||
|
letter-spacing: -0.035em;
|
||||||
|
}
|
||||||
|
.confirmation h1 em {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
.confirmation h2 {
|
||||||
|
margin: 0.5rem 0 1rem;
|
||||||
|
font-family: "Playfair Display", Georgia, serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.confirmation h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 1.5rem 0 0.65rem;
|
||||||
|
}
|
||||||
|
.confirmation__hero > p:not(.confirmation__eyebrow) {
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
.confirmation__simulation {
|
||||||
|
color: #cbd5e1;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-left: 2px solid #10b981;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.confirmation__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.confirmation__card {
|
||||||
|
border: 1px solid #ffffff14;
|
||||||
|
border-radius: 1.4rem;
|
||||||
|
padding: clamp(1.25rem, 3vw, 2.25rem);
|
||||||
|
background: linear-gradient(145deg, #ffffff06, #ffffff02);
|
||||||
|
}
|
||||||
|
.confirmation__sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.confirmation__muted,
|
||||||
|
.confirmation__steps p {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.confirmation__details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 0.75rem 1rem;
|
||||||
|
margin: 1.75rem 0 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.confirmation__details dt {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.confirmation__details dd {
|
||||||
|
margin: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.confirmation__details:not(.confirmation__details--stacked) dt:last-of-type,
|
||||||
|
.confirmation__details:not(.confirmation__details--stacked) dd:last-of-type {
|
||||||
|
border-top: 1px solid #ffffff20;
|
||||||
|
padding-top: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.confirmation__details--stacked {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.confirmation__details--stacked dd {
|
||||||
|
text-align: left;
|
||||||
|
margin: 0.2rem 0 1rem;
|
||||||
|
}
|
||||||
|
.confirmation__mail {
|
||||||
|
border-color: #10b98140;
|
||||||
|
background: #10b98108;
|
||||||
|
}
|
||||||
|
.confirmation__mail h2 {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.confirmation__mail p {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.confirmation__features {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.confirmation__features li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 0.8rem 0;
|
||||||
|
}
|
||||||
|
.confirmation__features li::before {
|
||||||
|
content: "✓";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
.confirmation__next {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.confirmation__steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
list-style: none;
|
||||||
|
counter-reset: steps;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.confirmation__steps li {
|
||||||
|
counter-increment: steps;
|
||||||
|
border-top: 1px solid #ffffff20;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
.confirmation__steps li::before {
|
||||||
|
content: "0" counter(steps);
|
||||||
|
color: #10b981;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.confirmation__steps h3 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.confirmation__steps p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.confirmation__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.confirmation__button,
|
||||||
|
.confirmation__link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
padding: 0.65rem 1.3rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.confirmation__button {
|
||||||
|
background: #10b981;
|
||||||
|
color: #071a14 !important;
|
||||||
|
}
|
||||||
|
.confirmation__link {
|
||||||
|
border: 1px solid #ffffff20;
|
||||||
|
}
|
||||||
|
.confirmation__button:hover {
|
||||||
|
background: #34d399;
|
||||||
|
}
|
||||||
|
.confirmation__link:hover {
|
||||||
|
background: #ffffff08;
|
||||||
|
}
|
||||||
|
.confirmation__empty {
|
||||||
|
max-width: 42rem;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
.confirmation__footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
border-top: 1px solid #ffffff12;
|
||||||
|
padding: 1.5rem clamp(1rem, 5vw, 5rem);
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
.confirmation__footer span {
|
||||||
|
letter-spacing: normal;
|
||||||
|
}
|
||||||
|
@media (max-width: 45rem) {
|
||||||
|
.confirmation__grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
.confirmation__main {
|
||||||
|
padding: 2.5rem 1rem;
|
||||||
|
}
|
||||||
|
.confirmation__header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.confirmation__brand small {
|
||||||
|
max-width: 12rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import "./booking-confirmation.css";
|
||||||
|
import { orderSummaryRows } from "../../shared/order-summary.js";
|
||||||
|
import { paymentNotice } from "../../shared/booking-copy.js";
|
||||||
|
|
||||||
|
const mailMessages = {
|
||||||
|
accepted:
|
||||||
|
"Die Bestätigung wurde vom Mailserver zum Versand angenommen. Die Zustellung ist noch nicht bestätigt. Bitte prüfe auch deinen Spam-Ordner.",
|
||||||
|
pending:
|
||||||
|
"Deine Buchung ist gespeichert. Der Bestätigungsversand steht noch aus. Du kannst den Versand mit derselben Buchung erneut versuchen.",
|
||||||
|
failed: "Deine Buchung ist gespeichert, aber der Bestätigungsversand ist fehlgeschlagen. Bitte buche nicht erneut. Wende dich mit deiner Buchungsnummer an Tri-Hub.",
|
||||||
|
unknown:
|
||||||
|
"Deine Buchung ist gespeichert. Der Versandstatus ist unklar. Bitte buche nicht erneut. Wende dich mit deiner Buchungsnummer an Tri-Hub.",
|
||||||
|
};
|
||||||
|
const select = (id) => document.getElementById(id);
|
||||||
|
function rows(id, values) {
|
||||||
|
const list = select(id);
|
||||||
|
for (const [label, value] of values) {
|
||||||
|
const term = document.createElement("dt");
|
||||||
|
const description = document.createElement("dd");
|
||||||
|
term.textContent = label;
|
||||||
|
description.textContent = value || "Nicht verfügbar";
|
||||||
|
list.append(term, description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur die Antwort der gespeicherten Buchung anzeigen, keine aktuellen
|
||||||
|
// Katalogpreise nachladen und keine erneute Buchungsanfrage auslösen.
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = JSON.parse(sessionStorage.getItem("trihub.booking.confirmation"));
|
||||||
|
} catch {
|
||||||
|
// Ohne lesbaren Sitzungsspeicher bleibt der Hinweis sichtbar.
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
body?.saved === true &&
|
||||||
|
typeof body.bookingId === "string" &&
|
||||||
|
body.bookingId
|
||||||
|
) {
|
||||||
|
const order = body.bookedPackage;
|
||||||
|
select("confirmation-payment").textContent = paymentNotice;
|
||||||
|
select("summary-title").textContent =
|
||||||
|
body.packageName || order?.title || "Dein Paket";
|
||||||
|
select("confirmation-summary").textContent = order?.summaryForBooking || "";
|
||||||
|
const date = new Date(body.createdAt);
|
||||||
|
rows("confirmation-receipt", [
|
||||||
|
["Buchungsnummer", body.bookingId],
|
||||||
|
[
|
||||||
|
"Buchungsdatum",
|
||||||
|
Number.isNaN(date.getTime())
|
||||||
|
? "Nicht verfügbar"
|
||||||
|
: new Intl.DateTimeFormat("de-DE", {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
}).format(date),
|
||||||
|
],
|
||||||
|
["E-Mail-Adresse", body.customerEmail],
|
||||||
|
[
|
||||||
|
"Buchungsstatus",
|
||||||
|
body.status === "CONFIRMED"
|
||||||
|
? "Bestätigt (Simulation)"
|
||||||
|
: "Gespeichert (Simulation)",
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
select("confirmation-mail-status").textContent =
|
||||||
|
mailMessages[body.emailStatus] || mailMessages.unknown;
|
||||||
|
if (order) {
|
||||||
|
rows("confirmation-totals", orderSummaryRows(order));
|
||||||
|
for (const feature of order.includedFeatures || []) {
|
||||||
|
const item = document.createElement("li");
|
||||||
|
item.textContent = feature;
|
||||||
|
select("confirmation-features").append(item);
|
||||||
|
}
|
||||||
|
select("confirmation-features-section").hidden =
|
||||||
|
!order.includedFeatures?.length;
|
||||||
|
for (const step of order.processSteps || []) {
|
||||||
|
const item = document.createElement("li");
|
||||||
|
const title = document.createElement("h3");
|
||||||
|
const text = document.createElement("p");
|
||||||
|
title.textContent = step.step;
|
||||||
|
text.textContent = step.text;
|
||||||
|
item.append(title, text);
|
||||||
|
select("confirmation-steps").append(item);
|
||||||
|
}
|
||||||
|
select("confirmation-steps-section").hidden =
|
||||||
|
!order.processSteps?.length;
|
||||||
|
} else {
|
||||||
|
select("confirmation-no-snapshot").hidden = false;
|
||||||
|
rows("confirmation-totals", [["Paket-ID", body.packageId]]);
|
||||||
|
}
|
||||||
|
if (body.emailStatus === "pending" && body.packageId) {
|
||||||
|
const params = new URLSearchParams({ id: body.packageId });
|
||||||
|
if (order?.variantId) params.set("variant", order.variantId);
|
||||||
|
select("confirmation-retry").href = `/booking.html?${params}`;
|
||||||
|
select("confirmation-retry").hidden = false;
|
||||||
|
}
|
||||||
|
select("confirmation-empty").hidden = true;
|
||||||
|
select("confirmation-content").hidden = false;
|
||||||
|
}
|
||||||
|
|
@ -3,16 +3,26 @@ import "./booking.css";
|
||||||
import { orderSummaryRows } from "../../shared/order-summary.js";
|
import { orderSummaryRows } from "../../shared/order-summary.js";
|
||||||
import { paymentNotice } from "../../shared/booking-copy.js";
|
import { paymentNotice } from "../../shared/booking-copy.js";
|
||||||
|
|
||||||
export function mountBookingPage(root, { packageId, variantId } = {}) {
|
// Die Detailseite kann dieselbe Section direkt mounten und später aufräumen.
|
||||||
|
// Die Zieladresse kann bei der Integration bei Bedarf überschrieben werden.
|
||||||
|
export function mountBookingPage(
|
||||||
|
root,
|
||||||
|
{
|
||||||
|
packageId,
|
||||||
|
variantId,
|
||||||
|
confirmationUrl = root.dataset.confirmationUrl ||
|
||||||
|
"/booking-confirmation.html",
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const { signal } = controller;
|
const { signal } = controller;
|
||||||
|
const instanceId = `booking-${crypto.randomUUID()}`;
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
<div class="booking">
|
<section class="booking" aria-labelledby="${instanceId}-heading">
|
||||||
<a href="/angebotsuebersicht.html">Zur Angebotsseite</a>
|
<h2 id="${instanceId}-heading">Paket buchen</h2>
|
||||||
<h1>Paket buchen</h1>
|
|
||||||
<p class="booking__intro" role="status">Paket wird geladen …</p>
|
<p class="booking__intro" role="status">Paket wird geladen …</p>
|
||||||
<div class="booking__content"></div>
|
<div class="booking__content"></div>
|
||||||
</div>`;
|
</section>`;
|
||||||
const intro = root.querySelector(".booking__intro");
|
const intro = root.querySelector(".booking__intro");
|
||||||
const content = root.querySelector(".booking__content");
|
const content = root.querySelector(".booking__content");
|
||||||
|
|
||||||
|
|
@ -68,28 +78,69 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||||
<ul class="booking__features"></ul>
|
<ul class="booking__features"></ul>
|
||||||
<div class="booking__steps"></div>
|
<div class="booking__steps"></div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="booking__payment" aria-labelledby="booking-payment-heading">
|
<aside class="booking__payment" aria-labelledby="${instanceId}-booking-payment-heading">
|
||||||
<h2 id="booking-payment-heading">Hinweis zum Buchungsablauf</h2>
|
<h2 id="${instanceId}-booking-payment-heading">Hinweis zum Buchungsablauf</h2>
|
||||||
<p class="booking__payment-notice"></p>
|
<p class="booking__payment-notice"></p>
|
||||||
</aside>
|
</aside>
|
||||||
|
<button class="booking__button booking__open" type="button" aria-haspopup="dialog" aria-controls="${instanceId}-dialog" aria-expanded="false">Jetzt buchen</button>
|
||||||
|
<dialog class="booking__dialog" id="${instanceId}-dialog" aria-labelledby="${instanceId}-dialog-heading" aria-describedby="${instanceId}-dialog-intro">
|
||||||
|
<button class="booking__close" type="button" aria-label="Buchungsdialog schließen">×</button>
|
||||||
|
<h2 id="${instanceId}-dialog-heading">Paket buchen</h2>
|
||||||
|
<p id="${instanceId}-dialog-intro">Bitte gib deine Kontaktdaten ein. Name und E-Mail-Adresse sind Pflichtfelder.</p>
|
||||||
<form novalidate>
|
<form novalidate>
|
||||||
<p>Alle Felder sind Pflichtfelder.</p>
|
|
||||||
<div class="booking__errors" role="alert" tabindex="-1" hidden></div>
|
<div class="booking__errors" role="alert" tabindex="-1" hidden></div>
|
||||||
<div class="booking__field">
|
<div class="booking__field">
|
||||||
<label for="booking-name">Name (Pflichtfeld)</label>
|
<label for="${instanceId}-booking-name">Name (Pflichtfeld)</label>
|
||||||
<input id="booking-name" name="name" type="text" autocomplete="name" required maxlength="120" aria-describedby="booking-name-error" />
|
<input id="${instanceId}-booking-name" name="name" type="text" autocomplete="name" required maxlength="120" aria-describedby="${instanceId}-booking-name-error" />
|
||||||
<p class="booking__field-error" id="booking-name-error"></p>
|
<p class="booking__field-error" id="${instanceId}-booking-name-error"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="booking__field">
|
<div class="booking__field">
|
||||||
<label for="booking-email">E-Mail-Adresse (Pflichtfeld)</label>
|
<label for="${instanceId}-booking-email">E-Mail-Adresse (Pflichtfeld)</label>
|
||||||
<input id="booking-email" name="email" type="email" autocomplete="email" inputmode="email" required maxlength="254" aria-describedby="booking-email-hint booking-email-error" />
|
<input id="${instanceId}-booking-email" name="email" type="email" autocomplete="email" inputmode="email" required maxlength="254" aria-describedby="${instanceId}-booking-email-hint ${instanceId}-booking-email-error" />
|
||||||
<p id="booking-email-hint">An diese Adresse senden wir deine Bestätigung.</p>
|
<p id="${instanceId}-booking-email-hint">An diese Adresse senden wir deine Bestätigung.</p>
|
||||||
<p class="booking__field-error" id="booking-email-error"></p>
|
<p class="booking__field-error" id="${instanceId}-booking-email-error"></p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="booking__field">
|
||||||
|
<label for="${instanceId}-booking-phone">Telefonnummer (optional)</label>
|
||||||
|
<input id="${instanceId}-booking-phone" name="phone" type="tel" autocomplete="tel" maxlength="50" aria-describedby="${instanceId}-booking-phone-hint ${instanceId}-booking-phone-error" />
|
||||||
|
<p id="${instanceId}-booking-phone-hint">Die Telefonnummer wird derzeit noch nicht an die Buchungsstelle übermittelt.</p>
|
||||||
|
<p class="booking__field-error" id="${instanceId}-booking-phone-error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="booking__actions">
|
||||||
|
<button class="booking__button booking__cancel" type="button">Abbrechen</button>
|
||||||
<button class="booking__button" type="submit">Paket buchen</button>
|
<button class="booking__button" type="submit">Paket buchen</button>
|
||||||
|
</div>
|
||||||
<p class="booking__progress" role="status"></p>
|
<p class="booking__progress" role="status"></p>
|
||||||
</form>
|
</form>
|
||||||
<div class="booking__result" role="status" tabindex="-1" hidden></div>`;
|
<div class="booking__result" role="status" tabindex="-1" hidden></div>
|
||||||
|
</dialog>`;
|
||||||
|
const dialog = content.querySelector("dialog");
|
||||||
|
const opener = content.querySelector(".booking__open");
|
||||||
|
opener.addEventListener(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
dialog.showModal();
|
||||||
|
opener.setAttribute("aria-expanded", "true");
|
||||||
|
const focusTarget =
|
||||||
|
content.querySelector(".booking__result:not([hidden])") ||
|
||||||
|
content.querySelector('[name="name"]');
|
||||||
|
focusTarget.focus();
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
for (const close of content.querySelectorAll(
|
||||||
|
".booking__close, .booking__cancel",
|
||||||
|
)) {
|
||||||
|
close.addEventListener("click", () => dialog.close(), { signal });
|
||||||
|
}
|
||||||
|
dialog.addEventListener(
|
||||||
|
"close",
|
||||||
|
() => {
|
||||||
|
opener.setAttribute("aria-expanded", "false");
|
||||||
|
opener.focus();
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
function renderOrder(order) {
|
function renderOrder(order) {
|
||||||
content.querySelector(".booking__package-name").textContent =
|
content.querySelector(".booking__package-name").textContent =
|
||||||
order.title;
|
order.title;
|
||||||
|
|
@ -133,7 +184,8 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||||
const form = content.querySelector("form");
|
const form = content.querySelector("form");
|
||||||
const name = form.elements.namedItem("name");
|
const name = form.elements.namedItem("name");
|
||||||
const email = form.elements.namedItem("email");
|
const email = form.elements.namedItem("email");
|
||||||
const button = form.querySelector("button");
|
const phone = form.elements.namedItem("phone");
|
||||||
|
const button = form.querySelector('button[type="submit"]');
|
||||||
const errors = form.querySelector(".booking__errors");
|
const errors = form.querySelector(".booking__errors");
|
||||||
const progress = form.querySelector(".booking__progress");
|
const progress = form.querySelector(".booking__progress");
|
||||||
const result = content.querySelector(".booking__result");
|
const result = content.querySelector(".booking__result");
|
||||||
|
|
@ -145,14 +197,15 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||||
function lockFields(locked) {
|
function lockFields(locked) {
|
||||||
name.readOnly = locked;
|
name.readOnly = locked;
|
||||||
email.readOnly = locked;
|
email.readOnly = locked;
|
||||||
|
phone.readOnly = locked;
|
||||||
}
|
}
|
||||||
function showError(message, fields = {}) {
|
function showError(message, fields = {}) {
|
||||||
errors.textContent = message;
|
errors.textContent = message;
|
||||||
errors.hidden = false;
|
errors.hidden = false;
|
||||||
for (const field of [name, email]) {
|
for (const field of [name, email, phone]) {
|
||||||
const message = fields[field.name] || "";
|
const message = fields[field.name] || "";
|
||||||
content.querySelector(
|
content.querySelector(
|
||||||
`#booking-${field.name}-error`,
|
`#${instanceId}-booking-${field.name}-error`,
|
||||||
).textContent = message;
|
).textContent = message;
|
||||||
field.setAttribute("aria-invalid", String(Boolean(message)));
|
field.setAttribute("aria-invalid", String(Boolean(message)));
|
||||||
}
|
}
|
||||||
|
|
@ -160,10 +213,10 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||||
}
|
}
|
||||||
function clearErrors() {
|
function clearErrors() {
|
||||||
errors.hidden = true;
|
errors.hidden = true;
|
||||||
for (const field of [name, email]) {
|
for (const field of [name, email, phone]) {
|
||||||
field.removeAttribute("aria-invalid");
|
field.removeAttribute("aria-invalid");
|
||||||
content.querySelector(
|
content.querySelector(
|
||||||
`#booking-${field.name}-error`,
|
`#${instanceId}-booking-${field.name}-error`,
|
||||||
).textContent = "";
|
).textContent = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,6 +252,7 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||||
if (stored.order) renderOrder(stored.order);
|
if (stored.order) renderOrder(stored.order);
|
||||||
name.value = stored.payload.name;
|
name.value = stored.payload.name;
|
||||||
email.value = stored.payload.email;
|
email.value = stored.payload.email;
|
||||||
|
phone.value = stored.phone || "";
|
||||||
lockFields(true);
|
lockFields(true);
|
||||||
button.textContent = "Status prüfen / erneut versuchen";
|
button.textContent = "Status prüfen / erneut versuchen";
|
||||||
if (stored.result) showResult(stored.result, false);
|
if (stored.result) showResult(stored.result, false);
|
||||||
|
|
@ -230,6 +284,13 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||||
if (!email.validity.valid || email.value.length > 254)
|
if (!email.validity.valid || email.value.length > 254)
|
||||||
fields.email =
|
fields.email =
|
||||||
"Bitte eine gültige E-Mail-Adresse eingeben.";
|
"Bitte eine gültige E-Mail-Adresse eingeben.";
|
||||||
|
phone.value = phone.value.trim();
|
||||||
|
if (
|
||||||
|
phone.value.length > 50 ||
|
||||||
|
/[\x00-\x1f\x7f]/.test(phone.value)
|
||||||
|
)
|
||||||
|
fields.phone =
|
||||||
|
"Bitte höchstens 50 Zeichen ohne Zeilenumbrüche eingeben.";
|
||||||
if (Object.keys(fields).length) {
|
if (Object.keys(fields).length) {
|
||||||
showError(
|
showError(
|
||||||
"Bitte die markierten Angaben prüfen.",
|
"Bitte die markierten Angaben prüfen.",
|
||||||
|
|
@ -240,6 +301,8 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||||
const nextAttempt = {
|
const nextAttempt = {
|
||||||
key: crypto.randomUUID(),
|
key: crypto.randomUUID(),
|
||||||
order: selected,
|
order: selected,
|
||||||
|
// Bis zur Backend-Erweiterung nur für die Wiederherstellung.
|
||||||
|
phone: phone.value,
|
||||||
payload: {
|
payload: {
|
||||||
packageId: selected.packageId,
|
packageId: selected.packageId,
|
||||||
...(selected.variantId
|
...(selected.variantId
|
||||||
|
|
@ -283,7 +346,26 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||||
} catch {
|
} catch {
|
||||||
/* Der ursprüngliche Schlüssel bleibt erhalten. */
|
/* Der ursprüngliche Schlüssel bleibt erhalten. */
|
||||||
}
|
}
|
||||||
if (!signal.aborted) showResult(body);
|
if (!signal.aborted) {
|
||||||
|
showResult(body);
|
||||||
|
if (
|
||||||
|
body.saved &&
|
||||||
|
body.emailStatus === "accepted" &&
|
||||||
|
confirmationUrl
|
||||||
|
) {
|
||||||
|
// Nur weiterleiten, wenn die Bestätigungsseite die
|
||||||
|
// Serverantwort in diesem Tab auch lesen kann.
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(
|
||||||
|
"trihub.booking.confirmation",
|
||||||
|
JSON.stringify(body),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return; // Die bestehende Bestätigung bleibt sichtbar.
|
||||||
|
}
|
||||||
|
window.location.assign(confirmationUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Nur eindeutige Ablehnungen vor dem Speichern erlauben eine
|
// Nur eindeutige Ablehnungen vor dem Speichern erlauben eine
|
||||||
// Korrektur. Bei Verbindungsfehlern bleiben Daten und Key fest.
|
// Korrektur. Bei Verbindungsfehlern bleiben Daten und Key fest.
|
||||||
|
|
@ -322,5 +404,9 @@ export function mountBookingPage(root, { packageId, variantId } = {}) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
load();
|
load();
|
||||||
return () => controller.abort();
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
root.querySelector(".booking__dialog")?.close();
|
||||||
|
root.replaceChildren();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,3 +117,76 @@
|
||||||
.booking__features li {
|
.booking__features li {
|
||||||
margin-block: 0.5rem;
|
margin-block: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sämtliche Dialogregeln bleiben innerhalb der Booking-Section. */
|
||||||
|
.booking .booking__dialog {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: min(32rem, calc(100vw - 2rem));
|
||||||
|
max-height: calc(100dvh - 2rem);
|
||||||
|
margin: auto;
|
||||||
|
padding: 1.75rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: #edf5f5;
|
||||||
|
background: #071316;
|
||||||
|
border: 1px solid #324449;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
box-shadow: 0 1.5rem 4rem #0006;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
.booking .booking__dialog::backdrop {
|
||||||
|
background: rgb(0 0 0 / 65%);
|
||||||
|
}
|
||||||
|
.booking__dialog h2 {
|
||||||
|
margin: 0 2rem 0.75rem 0;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
.booking .booking__close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.booking__dialog input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #101e22;
|
||||||
|
border-color: #61777d;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
.booking__dialog .booking__button {
|
||||||
|
background: #00b6c7;
|
||||||
|
border-color: #00b6c7;
|
||||||
|
color: #061619;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
.booking__dialog .booking__cancel {
|
||||||
|
background: transparent;
|
||||||
|
border-color: #61777d;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.booking__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.booking__actions > button {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.booking__dialog .booking__errors,
|
||||||
|
.booking__dialog .booking__field-error {
|
||||||
|
color: #ffb5b5;
|
||||||
|
}
|
||||||
|
.booking__dialog [aria-invalid="true"] {
|
||||||
|
border-color: #ffb5b5;
|
||||||
|
}
|
||||||
|
.booking__dialog :focus-visible,
|
||||||
|
.booking__dialog .booking__errors:focus,
|
||||||
|
.booking__dialog .booking__result:focus {
|
||||||
|
outline-color: #00d6e8;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ export default defineConfig({
|
||||||
new URL("./paket-details.html", import.meta.url),
|
new URL("./paket-details.html", import.meta.url),
|
||||||
),
|
),
|
||||||
main: fileURLToPath(new URL("./index.html", import.meta.url)),
|
main: fileURLToPath(new URL("./index.html", import.meta.url)),
|
||||||
|
bookingConfirmation: fileURLToPath(
|
||||||
|
new URL("./booking-confirmation.html", import.meta.url),
|
||||||
|
),
|
||||||
booking: fileURLToPath(
|
booking: fileURLToPath(
|
||||||
new URL("./booking.html", import.meta.url),
|
new URL("./booking.html", import.meta.url),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue