feat: Enhance client booking experience with booking name visibility and success page
This commit is contained in:
@@ -47,6 +47,28 @@ def client(appointment_id):
|
|||||||
if not available:
|
if not available:
|
||||||
return _appointment_not_found_response()
|
return _appointment_not_found_response()
|
||||||
|
|
||||||
|
current_user = str(session.get('username', '') or '').strip()
|
||||||
|
appointment_item = termin.get_item(appointment_id) or {}
|
||||||
|
appointment_owner = str(appointment_item.get('user', '') or '').strip()
|
||||||
|
can_view_booking_names = False
|
||||||
|
if current_user:
|
||||||
|
try:
|
||||||
|
can_view_booking_names = bool(us.check_admin(current_user) or current_user == appointment_owner)
|
||||||
|
except Exception:
|
||||||
|
can_view_booking_names = bool(current_user == appointment_owner)
|
||||||
|
|
||||||
|
available_for_view = dict(available)
|
||||||
|
if not can_view_booking_names:
|
||||||
|
sanitized_bookings = []
|
||||||
|
for booking in (available.get('slots_booked') or []):
|
||||||
|
if isinstance(booking, dict):
|
||||||
|
sanitized_bookings.append({'start': booking.get('start', '')})
|
||||||
|
elif isinstance(booking, (list, tuple)) and len(booking) >= 1:
|
||||||
|
sanitized_bookings.append({'start': booking[0]})
|
||||||
|
else:
|
||||||
|
sanitized_bookings.append({'start': ''})
|
||||||
|
available_for_view['slots_booked'] = sanitized_bookings
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
start_daytime = request.form.get('start_day_time')
|
start_daytime = request.form.get('start_day_time')
|
||||||
username = request.form.get('client_name')
|
username = request.form.get('client_name')
|
||||||
@@ -55,22 +77,50 @@ def client(appointment_id):
|
|||||||
return render_template(
|
return render_template(
|
||||||
'termin_client.html',
|
'termin_client.html',
|
||||||
appointment_id=appointment_id,
|
appointment_id=appointment_id,
|
||||||
available=available,
|
available=available_for_view,
|
||||||
current_user=session.get('username', ''),
|
current_user=session.get('username', ''),
|
||||||
|
tenant_id=_current_tenant_id(),
|
||||||
|
can_view_booking_names=can_view_booking_names,
|
||||||
)
|
)
|
||||||
|
|
||||||
if appointment_service.book_slot(appointment_id, start_daytime, username):
|
if appointment_service.book_slot(appointment_id, start_daytime, username):
|
||||||
flash('Der Termin wurde gespeichert.', 'success')
|
return redirect(
|
||||||
return redirect(url_for('terminplaner.client', appointment_id=appointment_id))
|
url_for(
|
||||||
|
'terminplaner.client_success',
|
||||||
|
appointment_id=appointment_id,
|
||||||
|
tenant=_current_tenant_id() or None,
|
||||||
|
start=start_daytime,
|
||||||
|
name=username,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
flash('Der Termin konnte nicht gespeichert werden.', 'error')
|
flash('Der Termin konnte nicht gespeichert werden.', 'error')
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'termin_client.html',
|
'termin_client.html',
|
||||||
appointment_id=appointment_id,
|
appointment_id=appointment_id,
|
||||||
available=available,
|
available=available_for_view,
|
||||||
current_user=session.get('username', ''),
|
current_user=session.get('username', ''),
|
||||||
tenant_id=_current_tenant_id(),
|
tenant_id=_current_tenant_id(),
|
||||||
|
can_view_booking_names=can_view_booking_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@appoint_bp.route('/client/success/<appointment_id>', methods=['GET'])
|
||||||
|
def client_success(appointment_id):
|
||||||
|
guard = _require_module_enabled()
|
||||||
|
if guard:
|
||||||
|
return guard
|
||||||
|
|
||||||
|
slot_start = str(request.args.get('start', '') or '').strip()
|
||||||
|
client_name = str(request.args.get('name', '') or '').strip()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'termin_client_success.html',
|
||||||
|
appointment_id=appointment_id,
|
||||||
|
slot_start=slot_start,
|
||||||
|
client_name=client_name,
|
||||||
|
tenant_id=_current_tenant_id(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+29
-22
@@ -1392,28 +1392,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark" id="loginNavbar">
|
<!-- No navbar for anonymous users on public pages. -->
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand py-0" href="{{ url_for('home') }}">
|
|
||||||
{% set school_logo_thumb = school_info.get('logo_thumb') if school_info else '' %}
|
|
||||||
{% set school_logo_path = school_info.get('logo_path') if school_info else '' %}
|
|
||||||
{% if school_logo_thumb or school_logo_path %}
|
|
||||||
{% if school_logo_thumb %}
|
|
||||||
<img src="{{ url_for('uploaded_file', filename=school_logo_thumb) }}" alt="{{ school_info.name or 'Schullogo' }}" class="invario-logo">
|
|
||||||
{% else %}
|
|
||||||
<img src="{{ url_for('uploaded_file', filename=school_logo_path) }}" alt="{{ school_info.name or 'Schullogo' }}" class="invario-logo">
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<img src="{{ url_for('static', filename='img/invario-logo.png') }}" alt="Invario" class="invario-logo">
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{{ url_for('impressum') }}">Impressum</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
@@ -1427,6 +1406,11 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if 'username' not in session %}
|
||||||
|
<div class="guest-impressum-footer">
|
||||||
|
<a href="{{ url_for('impressum') }}">Impressum</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<!-- Cookie consent banner -->
|
<!-- Cookie consent banner -->
|
||||||
<style>
|
<style>
|
||||||
#cookie-banner { position: fixed; bottom: 0; left: 0; right: 0; background: rgba(33,37,41,.98); color: #fff; padding: 14px 16px; display: none; z-index: 2000; box-shadow: 0 -2px 8px rgba(0,0,0,.25); }
|
#cookie-banner { position: fixed; bottom: 0; left: 0; right: 0; background: rgba(33,37,41,.98); color: #fff; padding: 14px 16px; display: none; z-index: 2000; box-shadow: 0 -2px 8px rgba(0,0,0,.25); }
|
||||||
@@ -1560,6 +1544,29 @@
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guest-impressum-footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 1200;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guest-impressum-footer a {
|
||||||
|
pointer-events: auto;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
color: rgba(15, 23, 42, 0.72);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guest-impressum-footer a:hover {
|
||||||
|
color: rgba(15, 23, 42, 0.92);
|
||||||
|
border-bottom-color: rgba(15, 23, 42, 0.52);
|
||||||
|
}
|
||||||
|
|
||||||
.notification-toast.show {
|
.notification-toast.show {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,14 @@
|
|||||||
<div class="fw-semibold mb-2">Bereits gebucht</div>
|
<div class="fw-semibold mb-2">Bereits gebucht</div>
|
||||||
<ul class="mb-0 small">
|
<ul class="mb-0 small">
|
||||||
{% for booking in available.slots_booked %}
|
{% for booking in available.slots_booked %}
|
||||||
<li>{{ booking.start }}{% if booking.name %} - {{ booking.name }}{% endif %}</li>
|
<li>
|
||||||
|
{{ booking.start }}
|
||||||
|
{% if can_view_booking_names and booking.name %}
|
||||||
|
- {{ booking.name }}
|
||||||
|
{% else %}
|
||||||
|
- Belegt
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Buchung erfolgreich</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg-a: #0f4c5c;
|
||||||
|
--bg-b: #16697a;
|
||||||
|
--ok: #22c55e;
|
||||||
|
--text: #0f172a;
|
||||||
|
--muted: #475569;
|
||||||
|
--card: #ffffff;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: "Manrope", "Segoe UI", sans-serif;
|
||||||
|
background: radial-gradient(circle at 20% 20%, rgba(255,255,255,0.14), transparent 45%), linear-gradient(135deg, var(--bg-a), var(--bg-b));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.success-window {
|
||||||
|
width: min(900px, 100%);
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
box-shadow: 0 28px 70px rgba(2, 6, 23, 0.32);
|
||||||
|
padding: clamp(1.5rem, 4vw, 3rem);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
width: 84px;
|
||||||
|
height: 84px;
|
||||||
|
margin: 0 auto 1.25rem auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgba(34, 197, 94, 0.12);
|
||||||
|
color: var(--ok);
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 0.9rem 0;
|
||||||
|
font-size: clamp(1.8rem, 5vw, 3rem);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
margin-top: 1.4rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
margin-top: 1.8rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
padding: .85rem 1.25rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.btn-close {
|
||||||
|
background: #0f4c5c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-link {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="success-window" role="main" aria-live="polite">
|
||||||
|
<div class="badge">✓</div>
|
||||||
|
<h1>Buchung erfolgreich</h1>
|
||||||
|
<p>Sie können das Fenster jetzt schließen.</p>
|
||||||
|
|
||||||
|
{% if client_name or slot_start %}
|
||||||
|
<div class="meta">
|
||||||
|
{% if client_name %}
|
||||||
|
<div>Name: {{ client_name }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if slot_start %}
|
||||||
|
<div>Termin: {{ slot_start }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-close" type="button" onclick="window.close()">Fenster schließen</button>
|
||||||
|
<a class="btn btn-link" href="{{ url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id) }}">Zurück zur Buchungsseite</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user