Owner calendar view + manual booking + owner cancel/reschedule (#20)

Adds an owner-authenticated weekly agenda (grouped by day, today highlighted)
with manual walk-in/phone booking creation, cancel, and reschedule -- all
routed through booking_api.py's create/cancel/reschedule logic (refactored
into shared helpers) so the EXCLUDE overlap constraint and confirmation
email stay on the single existing code path. Manual creation can skip the
opening-hours/min-notice/max-advance/buffer checks via an explicit override,
but never the overlap constraint itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 17:16:19 +02:00
parent 59c35ce39f
commit 528a13ca7c
8 changed files with 678 additions and 56 deletions
+150
View File
@@ -0,0 +1,150 @@
<!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>Kalender — {{ client.business_name if client else '' }}</title>
<style>
:root {
--brand: #0f8a7e;
--bg: #fff; --ink: #16302f; --muted: #5d716f; --line: #e3eae9;
--danger: #b3261e; --danger-bg: #fdecea;
}
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: var(--bg); color: var(--ink); padding: 20px; max-width: 760px; }
h1 { font-size: 1.2rem; margin: 0 0 6px; }
h2 { font-size: .95rem; margin: 0 0 8px; color: var(--muted); }
a { color: var(--brand); }
.nav { display: flex; align-items: center; justify-content: space-between; margin: 14px 0 20px; }
.nav a { text-decoration: none; font-size: .88rem; }
.day { border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; margin-bottom: 12px; }
.day.empty { color: var(--muted); }
.day.today { border-color: var(--brand); background: #f6faf9; }
.day.today h2 { color: var(--brand); }
table { width: 100%; border-collapse: collapse; font-size: .88rem; }
td, th { text-align: left; padding: 6px 4px; border-bottom: 1px solid var(--line); }
.status-cancelled { color: var(--muted); text-decoration: line-through; }
.status-pending { color: #9a6b00; }
form.inline { display: inline; }
input[type=text], input[type=datetime-local], select {
padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px;
font: inherit; font-size: .85rem; }
.btn { background: var(--brand); color: #fff; border: 0; border-radius: 7px;
padding: 6px 12px; font: inherit; font-size: .82rem; font-weight: 600; cursor: pointer; }
.btn-danger { background: var(--danger); }
.btn-small { padding: 4px 9px; font-size: .78rem; }
.error { background: var(--danger-bg); color: var(--danger); border-radius: 9px;
padding: 10px 14px; font-size: .88rem; margin-bottom: 14px; }
.new-booking { border: 1px solid var(--line); border-radius: 10px; padding: 16px; margin-top: 24px; }
.new-booking .field { display: inline-block; margin: 0 8px 8px 0; }
label { display: block; font-size: .75rem; color: var(--muted); margin-bottom: 3px; }
</style>
</head>
<body>
<h1>{{ client.business_name if client else 'Kalender' }}</h1>
{% if error == "missing_fields" %}
<div class="error">Bitte alle Felder ausfüllen.</div>
{% elif error == "not_found" %}
<div class="error">Ressource, Leistung oder Buchung nicht gefunden.</div>
{% elif error == "slot_taken" %}
<div class="error">Dieser Zeitraum überschneidet sich mit einem bestehenden Termin.</div>
{% elif error == "unavailable" %}
<div class="error">Dieser Termin liegt außerhalb der verfügbaren Zeiten.</div>
{% elif error == "already_cancelled" %}
<div class="error">Diese Buchung wurde bereits storniert.</div>
{% elif error == "bad_time" %}
<div class="error">Bitte eine gültige Uhrzeit angeben.</div>
{% endif %}
<div class="nav">
<a href="{{ url_for('owner_booking.agenda', week=prev_week) }}">&larr; Vorherige Woche</a>
<strong>Woche ab {{ week_start.strftime('%d.%m.%Y') }}</strong>
<a href="{{ url_for('owner_booking.agenda', week=next_week) }}">Nächste Woche &rarr;</a>
</div>
{% if week_start.isoformat() != this_week %}
<p><a href="{{ url_for('owner_booking.agenda', week=this_week) }}">&uarr; Zu heute springen</a></p>
{% endif %}
{% for day, bookings in days %}
<div class="day {{ 'today' if day == today else '' }} {{ 'empty' if not bookings }}">
<h2>{{ day.strftime('%A, %d.%m.%Y') }}{{ ' — Heute' if day == today else '' }}</h2>
{% if not bookings %}
<p class="muted" style="margin:0;">Keine Buchungen.</p>
{% else %}
<table>
<thead><tr><th>Zeit</th><th>Kunde</th><th>Leistung</th><th>Ressource</th><th>Status</th><th></th></tr></thead>
<tbody>
{% for b in bookings %}
<tr class="status-{{ b.status }}">
<td>{{ b.start_local.strftime('%H:%M') }}</td>
<td>{{ b.customer_name }}</td>
<td>{{ b.service }}</td>
<td>{{ b.resource_name }}</td>
<td>{{ b.status }}</td>
<td>
{% if b.status != "cancelled" %}
<form class="inline" method="post"
action="{{ url_for('owner_booking.reschedule_manual_booking', booking_id=b.booking_id) }}">
<input type="hidden" name="week" value="{{ week_start.isoformat() }}" />
<input type="datetime-local" name="start_time" required />
<button type="submit" class="btn btn-small">Verschieben</button>
</form>
<form class="inline" method="post"
action="{{ url_for('owner_booking.cancel_manual_booking', booking_id=b.booking_id) }}">
<input type="hidden" name="week" value="{{ week_start.isoformat() }}" />
<button type="submit" class="btn btn-danger btn-small">Stornieren</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% endfor %}
<div class="new-booking">
<h2>Neue Buchung (Laufkundschaft / Telefon)</h2>
<form method="post" action="{{ url_for('owner_booking.create_manual_booking') }}">
<input type="hidden" name="week" value="{{ week_start.isoformat() }}" />
<div class="field">
<label>Ressource</label>
<select name="resource_id" required>
{% for r in resources %}
<option value="{{ r.resource_id }}">{{ r.name }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>Leistung</label>
<select name="service_id" required>
{% for s in services %}
<option value="{{ s.service_id }}">{{ s.name }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>Datum &amp; Uhrzeit</label>
<input type="datetime-local" name="start_time" required />
</div>
<div class="field">
<label>Name</label>
<input type="text" name="customer_name" required />
</div>
<div class="field">
<label>Kontakt</label>
<input type="text" name="customer_contact" required />
</div>
<div class="field" style="vertical-align:bottom;">
<button type="submit" class="btn">Buchen</button>
</div>
</form>
</div>
<p class="muted" style="margin-top:20px;"><a href="{{ url_for('owner_auth.dashboard') }}">Zurück</a></p>
</body>
</html>