16500c4392
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>
162 lines
5.8 KiB
Python
162 lines
5.8 KiB
Python
"""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()
|