Owner settings: services, hours/buffer, auto-confirm, notify channel (#21)
Adds a session-authenticated /owner/settings blueprint for services CRUD (create/edit/deactivate), per-resource opening hours + min-notice/max-advance/ buffer, and client-level auto_confirm/notify_channel — all scoped to the logged-in owner's own client_id. Extends booking_db.py with the missing tenant-scoped update_service/update_resource/update_client writes, mirroring the existing update_booking allowlist pattern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
<!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>Einstellungen — {{ 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); }
|
||||
.muted { color: var(--muted); font-size: .85rem; }
|
||||
section { border: 1px solid var(--line); border-radius: 10px; padding: 16px; margin-bottom: 16px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: .88rem; margin-bottom: 10px; }
|
||||
td, th { text-align: left; padding: 6px 4px; border-bottom: 1px solid var(--line); }
|
||||
form.inline { display: inline; }
|
||||
form.row { display: flex; flex-wrap: wrap; align-items: end; gap: 8px; margin-bottom: 6px; }
|
||||
input[type=text], input[type=number], input[type=time], select {
|
||||
padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px;
|
||||
font: inherit; font-size: .85rem; }
|
||||
input[type=text] { width: 140px; }
|
||||
input[type=number] { width: 90px; }
|
||||
.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-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; }
|
||||
.field { display: inline-block; }
|
||||
label { display: block; font-size: .75rem; color: var(--muted); margin-bottom: 3px; }
|
||||
.weekday-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: .85rem; }
|
||||
.weekday-row .day-name { width: 90px; }
|
||||
.weekday-row label { display: inline; margin: 0 0 0 4px; font-size: .8rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ client.business_name if client else 'Einstellungen' }}</h1>
|
||||
|
||||
{% if error == "invalid_service" %}
|
||||
<div class="error">Bitte Name und eine gültige Dauer angeben.</div>
|
||||
{% elif error == "invalid_resource" %}
|
||||
<div class="error">Vorlaufzeit, Vorausbuchung und Puffer müssen 0 oder größer sein.</div>
|
||||
{% elif error == "invalid_hours" %}
|
||||
<div class="error">Bitte gültige Öffnungszeiten angeben (Ende nach Beginn).</div>
|
||||
{% elif error == "invalid_notify_channel" %}
|
||||
<div class="error">Nur Telegram ist derzeit als Benachrichtigungskanal verfügbar.</div>
|
||||
{% elif error == "not_found" %}
|
||||
<div class="error">Nicht gefunden.</div>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<h2>Leistungen</h2>
|
||||
{% if services %}
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Dauer (Min.)</th><th>Preis</th><th>Aktiv</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in services %}
|
||||
<tr>
|
||||
<form method="post" action="{{ url_for('owner_settings.update_service', service_id=s.service_id) }}">
|
||||
<td><input type="text" name="name" value="{{ s.name }}" required /></td>
|
||||
<td><input type="number" name="duration_minutes" value="{{ s.duration_minutes }}" min="1" required /></td>
|
||||
<td><input type="number" name="price" value="{{ s.price if s.price is not none else '' }}" step="0.01" min="0" /></td>
|
||||
<td><input type="checkbox" name="active" {{ 'checked' if s.active }} /></td>
|
||||
<td><button type="submit" class="btn btn-small">Speichern</button></td>
|
||||
</form>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">Noch keine Leistungen angelegt.</p>
|
||||
{% endif %}
|
||||
|
||||
<form class="row" method="post" action="{{ url_for('owner_settings.create_service') }}">
|
||||
<div class="field">
|
||||
<label>Neue Leistung</label>
|
||||
<input type="text" name="name" placeholder="Name" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Dauer (Min.)</label>
|
||||
<input type="number" name="duration_minutes" min="1" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Preis</label>
|
||||
<input type="number" name="price" step="0.01" min="0" />
|
||||
</div>
|
||||
<button type="submit" class="btn">Hinzufügen</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% for r in resources %}
|
||||
<section>
|
||||
<h2>Verfügbarkeit — {{ r.name }}</h2>
|
||||
<form class="row" method="post" action="{{ url_for('owner_settings.update_resource', resource_id=r.resource_id) }}">
|
||||
<div class="field">
|
||||
<label>Vorlaufzeit (Min.)</label>
|
||||
<input type="number" name="min_notice_minutes" value="{{ r.min_notice_minutes }}" min="0" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Vorausbuchung (Tage)</label>
|
||||
<input type="number" name="max_advance_days" value="{{ r.max_advance_days }}" min="0" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Puffer (Min.)</label>
|
||||
<input type="number" name="buffer_minutes" value="{{ r.buffer_minutes }}" min="0" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-small">Speichern</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ url_for('owner_settings.update_resource_hours', resource_id=r.resource_id) }}">
|
||||
{% set hours = hours_by_resource.get(r.resource_id, {}) %}
|
||||
{% set day_names = {0: 'Montag', 1: 'Dienstag', 2: 'Mittwoch', 3: 'Donnerstag',
|
||||
4: 'Freitag', 5: 'Samstag', 6: 'Sonntag'} %}
|
||||
{% for weekday in weekdays %}
|
||||
{% set day_hours = hours.get(weekday) %}
|
||||
<div class="weekday-row">
|
||||
<span class="day-name">{{ day_names[weekday] }}</span>
|
||||
<input type="time" name="opens_at_{{ weekday }}"
|
||||
value="{{ day_hours[0].strftime('%H:%M') if day_hours else '' }}" />
|
||||
<span>–</span>
|
||||
<input type="time" name="closes_at_{{ weekday }}"
|
||||
value="{{ day_hours[1].strftime('%H:%M') if day_hours else '' }}" />
|
||||
<input type="checkbox" name="closed_{{ weekday }}" id="closed_{{ r.resource_id }}_{{ weekday }}"
|
||||
{{ 'checked' if not day_hours }} />
|
||||
<label for="closed_{{ r.resource_id }}_{{ weekday }}">Geschlossen</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-small">Öffnungszeiten speichern</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
<section>
|
||||
<h2>Buchungen & Benachrichtigung</h2>
|
||||
<form class="row" method="post" action="{{ url_for('owner_settings.update_client') }}">
|
||||
<div class="field">
|
||||
<label>Automatisch bestätigen</label>
|
||||
<input type="checkbox" name="auto_confirm" {{ 'checked' if client and client.auto_confirm }} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Benachrichtigungskanal</label>
|
||||
<select name="notify_channel">
|
||||
<option value="" {{ 'selected' if not client or not client.notify_channel }}>Kein</option>
|
||||
<option value="telegram" {{ 'selected' if client and client.notify_channel == 'telegram' }}>Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn">Speichern</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<p class="muted"><a href="{{ url_for('owner_auth.dashboard') }}">Zurück</a></p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user