feat: Implement booking package and flow mappings with review page
This commit is contained in:
+74
-15
@@ -30,6 +30,34 @@ app.config["SESSION_COOKIE_SAMESITE"] = "Strict"
|
||||
app.config["SESSION_COOKIE_SECURE"] = os.environ.get("SESSION_COOKIE_SECURE", "0") == "1"
|
||||
app.config["PREFERRED_URL_SCHEME"] = "https" if os.environ.get("SESSION_COOKIE_SECURE") == "1" else "http"
|
||||
|
||||
BOOKING_PACKAGE_MAP = {
|
||||
"starter_package": "Starter-Paket",
|
||||
"advanced_package": "Advanced-Paket",
|
||||
"inventarsystem": "Inventarsystem",
|
||||
"buecherei": "Bücherei",
|
||||
"terminverwaltung": "Terminverwaltung",
|
||||
"emailversand": "E-Mail Versand",
|
||||
"schultraeger": "Schulträger Angebot",
|
||||
"telefonsupport": "Telefonsupport 20/Monat",
|
||||
}
|
||||
|
||||
BOOKING_FLOW_MAP = {
|
||||
"consultation": {
|
||||
"title": "Buchung als Gesprächstermin prüfen",
|
||||
"intro": "Bitte prüfen Sie die Angaben noch einmal. Nach Ihrer Bestätigung wird die Anfrage als Gesprächstermin an das Team gesendet.",
|
||||
"confirm_label": "Gesprächstermin bestätigen",
|
||||
"message_prefix": "Ich möchte für das Paket",
|
||||
"message_suffix": "einen Gesprächstermin vereinbaren. Bitte kontaktieren Sie mich für die nächsten Schritte.",
|
||||
},
|
||||
"payment": {
|
||||
"title": "Buchung vor dem Zahlungsschritt prüfen",
|
||||
"intro": "Bitte prüfen Sie die Angaben noch einmal. Nach Ihrer Bestätigung wird die Buchung per POST an /booking/payment übermittelt.",
|
||||
"confirm_label": "Jetzt kostenpflichtig buchen",
|
||||
"message_prefix": "Ich möchte das Paket",
|
||||
"message_suffix": "kostenpflichtig buchen. Bitte prüfen Sie die Angaben und senden Sie mir die nächsten Schritte.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.after_request
|
||||
def set_security_headers(response):
|
||||
@@ -1808,24 +1836,48 @@ def preise():
|
||||
@login_required
|
||||
def book_option_package():
|
||||
package_raw = _sanitize_text(request.form.get("package") or "", 40).lower()
|
||||
package_map = {
|
||||
"starter_package": "Starter-Paket",
|
||||
"advanced_package": "Advanced-Paket",
|
||||
"inventarsystem": "Inventarsystem",
|
||||
"buecherei": "Bücherei",
|
||||
"terminverwaltung": "Terminverwaltung",
|
||||
"emailversand": "E-Mail Versand",
|
||||
"schultraeger": "Schulträger Angebot",
|
||||
"telefonsupport": "Telefonsupport 20/Monat",
|
||||
}
|
||||
selected_package = package_map.get(package_raw)
|
||||
booking_flow = _sanitize_text(request.form.get("flow") or "payment", 24).lower()
|
||||
if booking_flow not in BOOKING_FLOW_MAP:
|
||||
booking_flow = "payment"
|
||||
|
||||
selected_package = BOOKING_PACKAGE_MAP.get(package_raw)
|
||||
if not selected_package:
|
||||
flash("Ungültige Buchungsoption ausgewählt.", "error")
|
||||
return redirect(url_for("preise"))
|
||||
|
||||
return redirect(url_for("booking_payment", package=package_raw, flow=booking_flow))
|
||||
|
||||
|
||||
@app.route('/booking/payment', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def booking_payment():
|
||||
package_raw = _sanitize_text((request.values.get("package") or ""), 40).lower()
|
||||
booking_flow = _sanitize_text((request.values.get("flow") or "payment"), 24).lower()
|
||||
if booking_flow not in BOOKING_FLOW_MAP:
|
||||
booking_flow = "payment"
|
||||
|
||||
selected_package = BOOKING_PACKAGE_MAP.get(package_raw)
|
||||
if not selected_package:
|
||||
flash("Ungültige Buchungsoption ausgewählt.", "error")
|
||||
return redirect(url_for("preise"))
|
||||
|
||||
flow_config = BOOKING_FLOW_MAP[booking_flow]
|
||||
review_data = {
|
||||
"package": selected_package,
|
||||
"package_key": package_raw,
|
||||
"flow": booking_flow,
|
||||
"title": flow_config["title"],
|
||||
"intro": flow_config["intro"],
|
||||
"confirm_label": flow_config["confirm_label"],
|
||||
"username": session.get("username"),
|
||||
"display_name": session.get("display_name") or session.get("username"),
|
||||
}
|
||||
|
||||
if request.method == "GET":
|
||||
return render_template("schnell_buchung.html", **review_data)
|
||||
|
||||
message = (
|
||||
f"Ich möchte das Paket {selected_package} buchen. "
|
||||
"Bitte kontaktieren Sie mich für die nächsten Schritte."
|
||||
f"{flow_config['message_prefix']} {selected_package} {flow_config['message_suffix']}"
|
||||
)
|
||||
|
||||
client = None
|
||||
@@ -1838,16 +1890,23 @@ def book_option_package():
|
||||
"sender_role": "user",
|
||||
"message": message,
|
||||
"created_at": _utc_now_iso(),
|
||||
"booking_flow": booking_flow,
|
||||
"booking_package": package_raw,
|
||||
}
|
||||
)
|
||||
except PyMongoError:
|
||||
flash("Buchungsanfrage konnte nicht gesendet werden.", "error")
|
||||
return redirect(url_for("preise"))
|
||||
return redirect(url_for("booking_payment", package=package_raw, flow=booking_flow))
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
flash(f"Buchungsanfrage für Paket {selected_package} wurde an den Admin gesendet.", "success")
|
||||
success_text = (
|
||||
f"Gesprächstermin für {selected_package} wurde an das Team gesendet."
|
||||
if booking_flow == "consultation"
|
||||
else f"Buchung für {selected_package} wurde an den Admin gesendet."
|
||||
)
|
||||
flash(success_text, "success")
|
||||
return redirect(url_for("user_chat"))
|
||||
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@
|
||||
</div>
|
||||
<div class="package-card__price">
|
||||
<span>150 €</span>
|
||||
<small>/ Jahr</small>
|
||||
<small>/ Jahr ab 400 Schüler</small>
|
||||
</div>
|
||||
</div>
|
||||
<p class="package-card__description">Ein spezialisiertes Modul für Bibliotheken und Medienzentren mit Ausleihprozessen, Medienbeständen und historischer Transparenz.</p>
|
||||
@@ -520,37 +520,46 @@
|
||||
<div class="custom-modal-backdrop" onclick="closeBookingModal()"></div>
|
||||
<div class="custom-modal-content">
|
||||
<h3>Buchung bestätigen</h3>
|
||||
<p>Möchten Sie dieses Paket wirklich anfragen?</p>
|
||||
<p style="font-size: 0.95rem;">Ein Administrator wird sich innerhalb von ca. 48 Stunden über das Portal mit Ihnen in Verbindung setzen. Dort wird dann ein Termin ausgemacht, um die Buchung persönlich vor Ort zu besprechen oder einen direkten Start zu vereinbaren.</p>
|
||||
<p>Möchten Sie dieses Paket jetzt direkt buchen oder zuerst einen Gesprächstermin vereinbaren?</p>
|
||||
<p style="font-size: 0.95rem;">In beiden Fällen werden Ihre Angaben noch einmal geprüft, bevor die Buchung per POST an /booking/payment gesendet wird.</p>
|
||||
<div class="custom-modal-actions">
|
||||
<button class="btn secondary" type="button" onclick="closeBookingModal()">Abbrechen</button>
|
||||
<button class="btn" type="button" id="confirmBookingBtn">Anfrage bestätigen</button>
|
||||
<button class="btn secondary" type="button" onclick="goToBookingReview('consultation')">Gesprächstermin vereinbaren</button>
|
||||
<button class="btn" type="button" onclick="goToBookingReview('payment')">Buchen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentFormToSubmit = null;
|
||||
const bookingReviewBaseUrl = "{{ url_for('booking_payment') }}";
|
||||
let currentBookingForm = null;
|
||||
|
||||
function openBookingModal(formElement) {
|
||||
currentFormToSubmit = formElement;
|
||||
currentBookingForm = formElement;
|
||||
const modal = document.getElementById('bookingModal');
|
||||
modal.classList.add('active');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
function closeBookingModal() {
|
||||
currentFormToSubmit = null;
|
||||
currentBookingForm = null;
|
||||
const modal = document.getElementById('bookingModal');
|
||||
modal.classList.remove('active');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
document.getElementById('confirmBookingBtn').addEventListener('click', function() {
|
||||
if (currentFormToSubmit) {
|
||||
currentFormToSubmit.submit();
|
||||
function goToBookingReview(flow) {
|
||||
if (!currentBookingForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageInput = currentBookingForm.querySelector('input[name="package"]');
|
||||
const packageValue = packageInput ? packageInput.value : '';
|
||||
const reviewUrl = new URL(bookingReviewBaseUrl, window.location.origin);
|
||||
reviewUrl.searchParams.set('package', packageValue);
|
||||
reviewUrl.searchParams.set('flow', flow);
|
||||
window.location.href = reviewUrl.toString();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Buchung prüfen{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
.booking-review {
|
||||
max-width: 920px;
|
||||
margin: 2rem auto 3rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.booking-review__panel {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 247, 244, 0.96));
|
||||
border: 1px solid rgba(28, 90, 99, 0.14);
|
||||
border-radius: 26px;
|
||||
padding: clamp(1.4rem, 2.6vw, 2.2rem);
|
||||
box-shadow: 0 18px 44px rgba(18, 49, 54, 0.08);
|
||||
}
|
||||
|
||||
.booking-review__eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.42rem 0.82rem;
|
||||
border-radius: 999px;
|
||||
background: #e8f4f4;
|
||||
color: #166271;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.booking-review__lead {
|
||||
margin-top: 0.75rem;
|
||||
max-width: 66ch;
|
||||
}
|
||||
|
||||
.booking-review__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.booking-review__card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #dbe6e7;
|
||||
border-radius: 18px;
|
||||
padding: 1rem 1.1rem;
|
||||
}
|
||||
|
||||
.booking-review__label {
|
||||
display: block;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
color: #5e757d;
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.booking-review__value {
|
||||
color: #173a42;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.booking-review__note {
|
||||
padding: 1rem 1.1rem;
|
||||
border-radius: 18px;
|
||||
background: #f8fbfb;
|
||||
border: 1px solid #dbe6e7;
|
||||
color: #49626b;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.booking-review__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.85rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.booking-review__actions .btn,
|
||||
.booking-review__actions a {
|
||||
width: auto;
|
||||
min-width: 190px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.booking-review__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.booking-review__actions .btn,
|
||||
.booking-review__actions a {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="booking-review">
|
||||
<div class="booking-review__panel">
|
||||
<span class="booking-review__eyebrow">Buchung prüfen</span>
|
||||
<h1>{{ title }}</h1>
|
||||
<p class="booking-review__lead">{{ intro }}</p>
|
||||
|
||||
<div class="booking-review__grid" aria-label="Buchungsübersicht">
|
||||
<div class="booking-review__card">
|
||||
<span class="booking-review__label">Paket</span>
|
||||
<div class="booking-review__value">{{ package }}</div>
|
||||
</div>
|
||||
<div class="booking-review__card">
|
||||
<span class="booking-review__label">Angemeldet als</span>
|
||||
<div class="booking-review__value">{{ display_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="booking-review__note">
|
||||
Bitte prüfen Sie die Angaben noch einmal. Wenn alles passt, senden Sie die Buchung mit dem folgenden Button per POST an /booking/payment.
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('booking_payment') }}" class="booking-review__actions">
|
||||
<input type="hidden" name="package" value="{{ package_key }}">
|
||||
<input type="hidden" name="flow" value="{{ flow }}">
|
||||
<button class="btn" type="submit">{{ confirm_label }}</button>
|
||||
<a class="btn secondary" href="{{ url_for('preise') }}">Zurück zu den Preisen</a>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user