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,161 @@
|
||||
"""Owner settings: services CRUD, per-resource opening hours/notice/buffer,
|
||||
auto_confirm, and notify_channel (#21).
|
||||
|
||||
All routes are session-authenticated via owner_auth.login_required and read
|
||||
client_id from the session only (never a request param), so every write goes
|
||||
through booking_db.py's own tenant-scoped UPDATE ... WHERE client_id = ...
|
||||
guard on top of that. Because booking_api.py's slot generation and booking
|
||||
creation always re-read resources/services/clients fresh on every request
|
||||
(#16, #18), a settings change here is live for the public booking page and
|
||||
availability engine on the very next request -- no cache to invalidate.
|
||||
"""
|
||||
from datetime import time
|
||||
|
||||
from flask import Blueprint, redirect, render_template, request, session, url_for
|
||||
|
||||
import booking_db as bdb
|
||||
from owner_auth import login_required
|
||||
|
||||
bp = Blueprint("owner_settings", __name__, url_prefix="/owner/settings")
|
||||
|
||||
WEEKDAYS = [0, 1, 2, 3, 4, 5, 6] # 0=Monday..6=Sunday, matching resource_hours
|
||||
|
||||
|
||||
def _redirect(error=None):
|
||||
return redirect(url_for("owner_settings.settings", error=error))
|
||||
|
||||
|
||||
def _parse_int(value):
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _parse_price(value):
|
||||
value = (value or "").strip()
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_time(value):
|
||||
value = (value or "").strip()
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return time.fromisoformat(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
@bp.get("")
|
||||
@login_required
|
||||
def settings():
|
||||
client_id = session["client_id"]
|
||||
client = bdb.get_client(client_id)
|
||||
resources = bdb.list_resources(client_id)
|
||||
hours_by_resource = {
|
||||
r["resource_id"]: bdb.get_resource_hours(client_id, r["resource_id"])
|
||||
for r in resources}
|
||||
return render_template(
|
||||
"owner/settings.html", client=client, services=bdb.list_services(client_id),
|
||||
resources=resources, hours_by_resource=hours_by_resource, weekdays=WEEKDAYS,
|
||||
error=request.args.get("error"))
|
||||
|
||||
|
||||
@bp.post("/services")
|
||||
@login_required
|
||||
def create_service():
|
||||
client_id = session["client_id"]
|
||||
name = (request.form.get("name") or "").strip()
|
||||
duration_minutes = _parse_int(request.form.get("duration_minutes"))
|
||||
price = _parse_price(request.form.get("price"))
|
||||
if not name or not duration_minutes or duration_minutes <= 0:
|
||||
return _redirect(error="invalid_service")
|
||||
bdb.create_service(client_id, name, duration_minutes, price=price)
|
||||
return _redirect()
|
||||
|
||||
|
||||
@bp.post("/services/<service_id>")
|
||||
@login_required
|
||||
def update_service(service_id):
|
||||
client_id = session["client_id"]
|
||||
name = (request.form.get("name") or "").strip()
|
||||
duration_minutes = _parse_int(request.form.get("duration_minutes"))
|
||||
price = _parse_price(request.form.get("price"))
|
||||
active = request.form.get("active") == "on"
|
||||
if not name or not duration_minutes or duration_minutes <= 0:
|
||||
return _redirect(error="invalid_service")
|
||||
row = bdb.update_service(
|
||||
client_id, service_id, name=name, duration_minutes=duration_minutes,
|
||||
price=price, active=active)
|
||||
if row is None:
|
||||
return _redirect(error="not_found")
|
||||
return _redirect()
|
||||
|
||||
|
||||
@bp.post("/resources/<resource_id>")
|
||||
@login_required
|
||||
def update_resource(resource_id):
|
||||
client_id = session["client_id"]
|
||||
min_notice_minutes = _parse_int(request.form.get("min_notice_minutes"))
|
||||
max_advance_days = _parse_int(request.form.get("max_advance_days"))
|
||||
buffer_minutes = _parse_int(request.form.get("buffer_minutes"))
|
||||
if min_notice_minutes is None or min_notice_minutes < 0:
|
||||
return _redirect(error="invalid_resource")
|
||||
if max_advance_days is None or max_advance_days < 0:
|
||||
return _redirect(error="invalid_resource")
|
||||
if buffer_minutes is None or buffer_minutes < 0:
|
||||
return _redirect(error="invalid_resource")
|
||||
row = bdb.update_resource(
|
||||
client_id, resource_id, min_notice_minutes=min_notice_minutes,
|
||||
max_advance_days=max_advance_days, buffer_minutes=buffer_minutes)
|
||||
if row is None:
|
||||
return _redirect(error="not_found")
|
||||
return _redirect()
|
||||
|
||||
|
||||
@bp.post("/resources/<resource_id>/hours")
|
||||
@login_required
|
||||
def update_resource_hours(resource_id):
|
||||
client_id = session["client_id"]
|
||||
if bdb.get_resource(client_id, resource_id) is None:
|
||||
return _redirect(error="not_found")
|
||||
|
||||
# Parse and validate every weekday up front so a bad entry later in the
|
||||
# form (e.g. Wednesday) can't leave earlier weekdays (Monday, Tuesday)
|
||||
# already written -- either the whole week's hours update or none of it
|
||||
# does, matching update_resource/update_client's all-or-nothing shape.
|
||||
parsed = {}
|
||||
for weekday in WEEKDAYS:
|
||||
if request.form.get(f"closed_{weekday}") == "on":
|
||||
parsed[weekday] = None
|
||||
continue
|
||||
opens_at = _parse_time(request.form.get(f"opens_at_{weekday}"))
|
||||
closes_at = _parse_time(request.form.get(f"closes_at_{weekday}"))
|
||||
if opens_at is None or closes_at is None or closes_at <= opens_at:
|
||||
return _redirect(error="invalid_hours")
|
||||
parsed[weekday] = (opens_at, closes_at)
|
||||
|
||||
for weekday, hours in parsed.items():
|
||||
if hours is None:
|
||||
bdb.set_resource_hours(client_id, resource_id, weekday, None, None)
|
||||
else:
|
||||
bdb.set_resource_hours(client_id, resource_id, weekday, *hours)
|
||||
return _redirect()
|
||||
|
||||
|
||||
@bp.post("/client")
|
||||
@login_required
|
||||
def update_client():
|
||||
client_id = session["client_id"]
|
||||
auto_confirm = request.form.get("auto_confirm") == "on"
|
||||
notify_channel = (request.form.get("notify_channel") or "").strip() or None
|
||||
if notify_channel is not None and notify_channel != "telegram":
|
||||
return _redirect(error="invalid_notify_channel")
|
||||
bdb.update_client(client_id, auto_confirm=auto_confirm, notify_channel=notify_channel)
|
||||
return _redirect()
|
||||
Reference in New Issue
Block a user