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:
@@ -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) }}">← 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 →</a>
|
||||
</div>
|
||||
{% if week_start.isoformat() != this_week %}
|
||||
<p><a href="{{ url_for('owner_booking.agenda', week=this_week) }}">↑ 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 & 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>
|
||||
@@ -23,7 +23,8 @@
|
||||
<h1>{{ client.business_name if client else 'Mein Konto' }}</h1>
|
||||
<div class="card">
|
||||
<p>Sie sind angemeldet.</p>
|
||||
<p class="muted">Kalender, Buchungen und Einstellungen folgen hier.</p>
|
||||
<p class="muted"><a href="{{ url_for('owner_booking.agenda') }}">Zum Kalender</a></p>
|
||||
<p class="muted">Einstellungen folgen hier.</p>
|
||||
</div>
|
||||
<p class="muted"><a href="{{ url_for('owner_auth.logout') }}">Abmelden</a></p>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user