From ee41e30d5bfb68a31327521569e609bf300da478 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 28 Feb 2026 18:15:35 +0000 Subject: [PATCH] Move payments admin from events to cart service Payments config (SumUp credentials per page) is a cart concern since all checkouts go through the cart service. Moves it from events.rose-ash.com to cart.rose-ash.com//admin/payments/ and adds a cart admin overview page at //admin/. Co-Authored-By: Claude Opus 4.6 --- cart/app.py | 7 + cart/bp/__init__.py | 1 + .../bp/page_admin}/__init__.py | 0 cart/bp/page_admin/routes.py | 83 ++ cart/sexp/payments.sexpr | 21 + cart/sexp/sexp_components.py | 123 +++ events/app.py | 12 +- events/bp/__init__.py | 1 - events/bp/payments/routes.py | 90 -- events/sexp/header.sexpr | 3 - events/sexp/markets.sexpr | 39 + events/sexp/sexp_components.py | 944 ++++++++++++++++-- 12 files changed, 1151 insertions(+), 173 deletions(-) rename {events/bp/payments => cart/bp/page_admin}/__init__.py (100%) create mode 100644 cart/bp/page_admin/routes.py create mode 100644 cart/sexp/payments.sexpr delete mode 100644 events/bp/payments/routes.py create mode 100644 events/sexp/markets.sexpr diff --git a/cart/app.py b/cart/app.py index f361e3f..eafcf8f 100644 --- a/cart/app.py +++ b/cart/app.py @@ -16,6 +16,7 @@ from bp import ( register_cart_overview, register_page_cart, register_cart_global, + register_page_admin, register_fragments, register_actions, register_data, @@ -195,6 +196,12 @@ def create_app() -> "Quart": url_prefix="/", ) + # Page admin at //admin/ (before page_cart catch-all) + app.register_blueprint( + register_page_admin(), + url_prefix="//admin", + ) + # Page cart at // (dynamic, matched last) app.register_blueprint( register_page_cart(url_prefix="/"), diff --git a/cart/bp/__init__.py b/cart/bp/__init__.py index ee967da..a48e533 100644 --- a/cart/bp/__init__.py +++ b/cart/bp/__init__.py @@ -1,6 +1,7 @@ from .cart.overview_routes import register as register_cart_overview from .cart.page_routes import register as register_page_cart from .cart.global_routes import register as register_cart_global +from .page_admin.routes import register as register_page_admin from .fragments import register_fragments from .actions import register_actions from .data import register_data diff --git a/events/bp/payments/__init__.py b/cart/bp/page_admin/__init__.py similarity index 100% rename from events/bp/payments/__init__.py rename to cart/bp/page_admin/__init__.py diff --git a/cart/bp/page_admin/routes.py b/cart/bp/page_admin/routes.py new file mode 100644 index 0000000..c53798b --- /dev/null +++ b/cart/bp/page_admin/routes.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from quart import ( + make_response, Blueprint, g, request +) + +from shared.infrastructure.actions import call_action +from shared.infrastructure.data_client import fetch_data +from shared.browser.app.authz import require_admin +from shared.browser.app.utils.htmx import is_htmx_request + + +def register(): + bp = Blueprint("page_admin", __name__) + + @bp.get("/") + @require_admin + async def admin(**kwargs): + from shared.sexp.page import get_template_context + from sexp.sexp_components import render_cart_admin_page, render_cart_admin_oob + + ctx = await get_template_context() + page_post = getattr(g, "page_post", None) + if not is_htmx_request(): + html = await render_cart_admin_page(ctx, page_post) + else: + html = await render_cart_admin_oob(ctx, page_post) + return await make_response(html) + + @bp.get("/payments/") + @require_admin + async def payments(**kwargs): + from shared.sexp.page import get_template_context + from sexp.sexp_components import render_cart_payments_page, render_cart_payments_oob + + ctx = await get_template_context() + page_post = getattr(g, "page_post", None) + if not is_htmx_request(): + html = await render_cart_payments_page(ctx, page_post) + else: + html = await render_cart_payments_oob(ctx, page_post) + return await make_response(html) + + @bp.put("/payments/") + @require_admin + async def update_sumup(**kwargs): + """Update SumUp credentials for this page (writes to blog's db_blog).""" + page_post = getattr(g, "page_post", None) + if not page_post: + return await make_response("Page not found", 404) + + form = await request.form + merchant_code = (form.get("merchant_code") or "").strip() + api_key = (form.get("api_key") or "").strip() + checkout_prefix = (form.get("checkout_prefix") or "").strip() + + payload = { + "container_type": "page", + "container_id": page_post.id, + "sumup_merchant_code": merchant_code, + "sumup_checkout_prefix": checkout_prefix, + } + if api_key: + payload["sumup_api_key"] = api_key + + await call_action("blog", "update-page-config", payload=payload) + + # Re-fetch page config to get fresh data + from types import SimpleNamespace + raw_pc = await fetch_data( + "blog", "page-config", + params={"container_type": "page", "container_id": page_post.id}, + required=False, + ) + g.page_config = SimpleNamespace(**raw_pc) if raw_pc else None + + from shared.sexp.page import get_template_context + from sexp.sexp_components import render_cart_payments_panel + ctx = await get_template_context() + html = render_cart_payments_panel(ctx) + return await make_response(html) + + return bp diff --git a/cart/sexp/payments.sexpr b/cart/sexp/payments.sexpr new file mode 100644 index 0000000..69e559b --- /dev/null +++ b/cart/sexp/payments.sexpr @@ -0,0 +1,21 @@ +;; Cart payments components + +(defcomp ~cart-payments-panel (&key update-url csrf merchant-code placeholder input-cls sumup-configured checkout-prefix) + (section :class "p-4 max-w-lg mx-auto" + (div :id "payments-panel" :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200" + (h3 :class "text-lg font-semibold text-stone-800" + (i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment") + (p :class "text-xs text-stone-400" "Configure per-page SumUp credentials. Leave blank to use the global merchant account.") + (form :hx-put update-url :hx-target "#payments-panel" :hx-swap "outerHTML" :hx-select "#payments-panel" :class "space-y-3" + (input :type "hidden" :name "csrf_token" :value csrf) + (div (label :class "block text-xs font-medium text-stone-600 mb-1" "Merchant Code") + (input :type "text" :name "merchant_code" :value merchant-code :placeholder "e.g. ME4J6100" :class input-cls)) + (div (label :class "block text-xs font-medium text-stone-600 mb-1" "API Key") + (input :type "password" :name "api_key" :value "" :placeholder placeholder :class input-cls) + (when sumup-configured (p :class "text-xs text-stone-400 mt-0.5" "Key is set. Leave blank to keep current key."))) + (div (label :class "block text-xs font-medium text-stone-600 mb-1" "Checkout Reference Prefix") + (input :type "text" :name "checkout_prefix" :value checkout-prefix :placeholder "e.g. ROSE-" :class input-cls)) + (button :type "submit" :class "px-4 py-1.5 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700 focus:ring-2 focus:ring-purple-500" + "Save SumUp Settings") + (when sumup-configured (span :class "ml-2 text-xs text-green-600" + (i :class "fa fa-check-circle") " Connected")))))) diff --git a/cart/sexp/sexp_components.py b/cart/sexp/sexp_components.py index 74ce5e1..7f9321e 100644 --- a/cart/sexp/sexp_components.py +++ b/cart/sexp/sexp_components.py @@ -721,3 +721,126 @@ async def render_checkout_error_page(ctx: dict, error: str | None = None, order: filt = _checkout_error_filter_html() content = _checkout_error_content_html(error, order) return full_page(ctx, header_rows_html=hdr, filter_html=filt, content_html=content) + + +# --------------------------------------------------------------------------- +# Page admin (//admin/) +# --------------------------------------------------------------------------- + +def _cart_page_admin_header_html(ctx: dict, page_post: Any, *, oob: bool = False) -> str: + """Build the page-level admin header row.""" + from quart import url_for + link_href = url_for("page_admin.admin") + return render("menu-row", id="page-admin-row", level=2, colour="sky", + link_href=link_href, link_label="admin", icon="fa fa-cog", + child_id="page-admin-header-child", oob=oob) + + +def _cart_payments_header_html(ctx: dict, *, oob: bool = False) -> str: + """Build the payments section header row.""" + from quart import url_for + link_href = url_for("page_admin.payments") + return render("menu-row", id="payments-row", level=3, colour="sky", + link_href=link_href, link_label="Payments", + icon="fa fa-credit-card", + child_id="payments-header-child", oob=oob) + + +def _cart_admin_main_panel_html(ctx: dict) -> str: + """Admin overview panel — links to sub-admin pages.""" + from quart import url_for + payments_href = url_for("page_admin.payments") + return ( + '
' + '
' + ' Payments' + f'configure' + '
' + '
' + ) + + +def _cart_payments_main_panel_html(ctx: dict) -> str: + """Render SumUp payment config form.""" + from quart import url_for + csrf_token = ctx.get("csrf_token") + csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") + page_config = ctx.get("page_config") + sumup_configured = bool(page_config and getattr(page_config, "sumup_api_key", None)) + merchant_code = (getattr(page_config, "sumup_merchant_code", None) or "") if page_config else "" + checkout_prefix = (getattr(page_config, "sumup_checkout_prefix", None) or "") if page_config else "" + update_url = url_for("page_admin.update_sumup") + + placeholder = "--------" if sumup_configured else "sup_sk_..." + input_cls = "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500" + + return render("cart-payments-panel", + update_url=update_url, csrf=csrf, + merchant_code=merchant_code, placeholder=placeholder, + input_cls=input_cls, sumup_configured=sumup_configured, + checkout_prefix=checkout_prefix) + + +# --------------------------------------------------------------------------- +# Public API: Cart page admin +# --------------------------------------------------------------------------- + +async def render_cart_admin_page(ctx: dict, page_post: Any) -> str: + """Full page: cart page admin overview.""" + content = _cart_admin_main_panel_html(ctx) + hdr = root_header_html(ctx) + child = _page_cart_header_html(ctx, page_post) + _cart_page_admin_header_html(ctx, page_post) + hdr += render("cart-header-child-nested", + outer_html=_cart_header_html(ctx), inner_html=child) + return full_page(ctx, header_rows_html=hdr, content_html=content) + + +async def render_cart_admin_oob(ctx: dict, page_post: Any) -> str: + """OOB response: cart page admin overview.""" + content = _cart_admin_main_panel_html(ctx) + oobs = ( + _cart_page_admin_header_html(ctx, page_post, oob=True) + + render("cart-header-child-oob", + inner_html=_page_cart_header_html(ctx, page_post) + + _cart_page_admin_header_html(ctx, page_post)) + + _cart_header_html(ctx, oob=True) + + root_header_html(ctx, oob=True) + ) + return oob_page(ctx, oobs_html=oobs, content_html=content) + + +# --------------------------------------------------------------------------- +# Public API: Cart payments admin +# --------------------------------------------------------------------------- + +async def render_cart_payments_page(ctx: dict, page_post: Any) -> str: + """Full page: payments config.""" + content = _cart_payments_main_panel_html(ctx) + hdr = root_header_html(ctx) + admin_hdr = _cart_page_admin_header_html(ctx, page_post) + payments_hdr = _cart_payments_header_html(ctx) + child = _page_cart_header_html(ctx, page_post) + admin_hdr + payments_hdr + hdr += render("cart-header-child-nested", + outer_html=_cart_header_html(ctx), inner_html=child) + return full_page(ctx, header_rows_html=hdr, content_html=content) + + +async def render_cart_payments_oob(ctx: dict, page_post: Any) -> str: + """OOB response: payments config.""" + content = _cart_payments_main_panel_html(ctx) + admin_hdr = _cart_page_admin_header_html(ctx, page_post) + payments_hdr = _cart_payments_header_html(ctx) + oobs = ( + _cart_payments_header_html(ctx, oob=True) + + render("cart-header-child-oob", + inner_html=_page_cart_header_html(ctx, page_post) + + admin_hdr + payments_hdr) + + _cart_header_html(ctx, oob=True) + + root_header_html(ctx, oob=True) + ) + return oob_page(ctx, oobs_html=oobs, content_html=content) + + +def render_cart_payments_panel(ctx: dict) -> str: + """Render the payments config panel for PUT response.""" + return _cart_payments_main_panel_html(ctx) diff --git a/events/app.py b/events/app.py index 154ff21..30e8d85 100644 --- a/events/app.py +++ b/events/app.py @@ -9,7 +9,7 @@ from jinja2 import FileSystemLoader, ChoiceLoader from shared.infrastructure.factory import create_base_app -from bp import register_all_events, register_calendar, register_calendars, register_markets, register_payments, register_page, register_fragments, register_actions, register_data +from bp import register_all_events, register_calendar, register_calendars, register_markets, register_page, register_fragments, register_actions, register_data async def events_context() -> dict: @@ -93,13 +93,13 @@ def create_app() -> "Quart": # Individual calendars at /// app.register_blueprint( register_calendar(), - url_prefix="/", + url_prefix="//", ) # Calendar admin under post slug: //admin/ app.register_blueprint( register_calendars(), - url_prefix="/", + url_prefix="//admin", ) # Markets nested under post slug: //markets/... @@ -108,12 +108,6 @@ def create_app() -> "Quart": url_prefix="//markets", ) - # Payments nested under post slug: //payments/... - app.register_blueprint( - register_payments(), - url_prefix="//payments", - ) - app.register_blueprint(register_fragments()) app.register_blueprint(register_actions()) app.register_blueprint(register_data()) diff --git a/events/bp/__init__.py b/events/bp/__init__.py index 8f6bb2b..cbef22c 100644 --- a/events/bp/__init__.py +++ b/events/bp/__init__.py @@ -2,7 +2,6 @@ from .all_events.routes import register as register_all_events from .calendar.routes import register as register_calendar from .calendars.routes import register as register_calendars from .markets.routes import register as register_markets -from .payments.routes import register as register_payments from .page.routes import register as register_page from .fragments import register_fragments from .actions import register_actions diff --git a/events/bp/payments/routes.py b/events/bp/payments/routes.py deleted file mode 100644 index 8e4da9a..0000000 --- a/events/bp/payments/routes.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import annotations - -from types import SimpleNamespace - -from quart import ( - render_template, make_response, Blueprint, g, request -) - -from shared.infrastructure.data_client import fetch_data -from shared.infrastructure.actions import call_action -from shared.browser.app.authz import require_admin -from shared.browser.app.utils.htmx import is_htmx_request - - -def register(): - bp = Blueprint("payments", __name__, url_prefix='/payments') - - @bp.context_processor - async def inject_root(): - return {} - - async def _load_payment_ctx(): - """Load PageConfig SumUp data for the current page (from blog).""" - post = (getattr(g, "post_data", None) or {}).get("post", {}) - post_id = post.get("id") - if not post_id: - return {} - - raw_pc = await fetch_data( - "blog", "page-config", - params={"container_type": "page", "container_id": post_id}, - required=False, - ) - pc = SimpleNamespace(**raw_pc) if raw_pc else None - - return { - "sumup_configured": bool(pc and getattr(pc, "sumup_api_key", None)), - "sumup_merchant_code": (getattr(pc, "sumup_merchant_code", None) or "") if pc else "", - "sumup_checkout_prefix": (getattr(pc, "sumup_checkout_prefix", None) or "") if pc else "", - } - - @bp.get("/") - @require_admin - async def home(**kwargs): - pay_ctx = await _load_payment_ctx() - - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_payments_page, render_payments_oob - - ctx = await get_template_context() - ctx.update(pay_ctx) - if not is_htmx_request(): - html = await render_payments_page(ctx) - else: - html = await render_payments_oob(ctx) - return await make_response(html) - - @bp.put("/") - @require_admin - async def update_sumup(**kwargs): - """Update SumUp credentials for this page (writes to blog's db_blog).""" - post = (getattr(g, "post_data", None) or {}).get("post", {}) - post_id = post.get("id") - if not post_id: - return await make_response("Post not found", 404) - - form = await request.form - merchant_code = (form.get("merchant_code") or "").strip() - api_key = (form.get("api_key") or "").strip() - checkout_prefix = (form.get("checkout_prefix") or "").strip() - - payload = { - "container_type": "page", - "container_id": post_id, - "sumup_merchant_code": merchant_code, - "sumup_checkout_prefix": checkout_prefix, - } - if api_key: - payload["sumup_api_key"] = api_key - - await call_action("blog", "update-page-config", payload=payload) - - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_payments_panel - ctx = await get_template_context() - ctx.update(await _load_payment_ctx()) - html = render_payments_panel(ctx) - return await make_response(html) - - return bp diff --git a/events/sexp/header.sexpr b/events/sexp/header.sexpr index 03c7872..84d6990 100644 --- a/events/sexp/header.sexpr +++ b/events/sexp/header.sexpr @@ -21,9 +21,6 @@ (defcomp ~events-markets-label () (<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div "Markets"))) -(defcomp ~events-payments-label () - (<> (i :class "fa fa-credit-card" :aria-hidden "true") (div "Payments"))) - (defcomp ~events-calendar-label (&key name description) (div :class "flex flex-col md:flex-row md:gap-2 items-center min-w-0" (div :class "flex flex-row items-center gap-2" diff --git a/events/sexp/markets.sexpr b/events/sexp/markets.sexpr new file mode 100644 index 0000000..315bd59 --- /dev/null +++ b/events/sexp/markets.sexpr @@ -0,0 +1,39 @@ +;; Events markets components + +(defcomp ~events-markets-create-form (&key create-url csrf) + (<> + (div :id "market-create-errors" :class "mt-2 text-sm text-red-600") + (form :class "mt-4 flex gap-2 items-end" :hx-post create-url + :hx-target "#markets-list" :hx-select "#markets-list" :hx-swap "outerHTML" + :hx-on::before-request "document.querySelector('#market-create-errors').textContent='';" + :hx-on::response-error "document.querySelector('#market-create-errors').innerHTML = event.detail.xhr.responseText;" + (input :type "hidden" :name "csrf_token" :value csrf) + (div :class "flex-1" + (label :class "block text-sm text-gray-600" "Name") + (input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2" + :placeholder "e.g. Farm Shop, Bakery")) + (button :type "submit" :class "border rounded px-3 py-2" "Add market")))) + +(defcomp ~events-markets-panel (&key form-html list-html) + (section :class "p-4" + (raw! form-html) + (div :id "markets-list" :class "mt-6" (raw! list-html)))) + +(defcomp ~events-markets-empty () + (p :class "text-gray-500 mt-4" "No markets yet. Create one above.")) + +(defcomp ~events-markets-item (&key href market-name market-slug del-url csrf-hdr) + (div :class "mt-6 border rounded-lg p-4" + (div :class "flex items-center justify-between gap-3" + (a :class "flex items-baseline gap-3" :href href + (h3 :class "font-semibold" market-name) + (h4 :class "text-gray-500" (str "/" market-slug "/"))) + (button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400" + :data-confirm true :data-confirm-title "Delete market?" + :data-confirm-text "Products will be hidden (soft delete)" + :data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it" + :data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed" + :hx-delete del-url :hx-trigger "confirmed" + :hx-target "#markets-list" :hx-select "#markets-list" :hx-swap "outerHTML" + :hx-headers csrf-hdr + (i :class "fa-solid fa-trash"))))) diff --git a/events/sexp/sexp_components.py b/events/sexp/sexp_components.py index 09aba6e..d0039d5 100644 --- a/events/sexp/sexp_components.py +++ b/events/sexp/sexp_components.py @@ -2,7 +2,7 @@ Events service s-expression page components. Renders all events, page summary, calendars, calendar month, day, day admin, -calendar admin, tickets, ticket admin, markets, and payments pages. +calendar admin, tickets, ticket admin, and markets pages. Called from route handlers in place of ``render_template()``. """ from __future__ import annotations @@ -87,6 +87,26 @@ def _post_nav_html(ctx: dict) -> str: if container_nav: parts.append(container_nav) + # Admin cog → blog admin for this post (cross-domain, no HTMX) + rights = ctx.get("rights") or {} + has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False) + if has_admin: + post = ctx.get("post") or {} + slug = post.get("slug", "") + styles = ctx.get("styles") or {} + nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "") + select_colours = ctx.get("select_colours", "") + admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/") + aclass = f"{nav_btn} {select_colours}".strip() or ( + "justify-center cursor-pointer flex flex-row items-center gap-2 " + "rounded bg-stone-200 text-black p-3" + ) + parts.append( + f'' + ) + return "".join(parts) @@ -301,20 +321,6 @@ def _markets_header_html(ctx: dict, *, oob: bool = False) -> str: child_id="markets-header-child", oob=oob) -# --------------------------------------------------------------------------- -# Payments header -# --------------------------------------------------------------------------- - -def _payments_header_html(ctx: dict, *, oob: bool = False) -> str: - """Build the payments section header row.""" - from quart import url_for - link_href = url_for("payments.home") - return render("menu-row", id="payments-row", level=3, - link_href=link_href, - link_label_html=render("events-payments-label"), - child_id="payments-header-child", oob=oob) - - # --------------------------------------------------------------------------- # Calendars main panel # --------------------------------------------------------------------------- @@ -699,30 +705,6 @@ def _markets_list_html(ctx: dict, markets: list) -> str: return "".join(parts) -# --------------------------------------------------------------------------- -# Payments main panel -# --------------------------------------------------------------------------- - -def _payments_main_panel_html(ctx: dict) -> str: - """Render SumUp payment config form.""" - from quart import url_for - csrf_token = ctx.get("csrf_token") - csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") - sumup_configured = ctx.get("sumup_configured", False) - merchant_code = ctx.get("sumup_merchant_code", "") - checkout_prefix = ctx.get("sumup_checkout_prefix", "") - update_url = url_for("payments.update_sumup") - - placeholder = "--------" if sumup_configured else "sup_sk_..." - input_cls = "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500" - - return render("events-payments-panel", - update_url=update_url, csrf=csrf, - merchant_code=merchant_code, placeholder=placeholder, - input_cls=input_cls, sumup_configured=sumup_configured, - checkout_prefix=checkout_prefix) - - # --------------------------------------------------------------------------- # Ticket state badge helper # --------------------------------------------------------------------------- @@ -1507,28 +1489,6 @@ async def render_markets_oob(ctx: dict) -> str: return oob_page(ctx, oobs_html=oobs, content_html=content) -# --------------------------------------------------------------------------- -# Payments -# --------------------------------------------------------------------------- - -async def render_payments_page(ctx: dict) -> str: - """Full page: payments admin.""" - content = _payments_main_panel_html(ctx) - hdr = root_header_html(ctx) - child = _post_header_html(ctx) + _payments_header_html(ctx) - hdr += render("events-header-child", inner_html=child) - return full_page(ctx, header_rows_html=hdr, content_html=content) - - -async def render_payments_oob(ctx: dict) -> str: - """OOB response: payments admin.""" - content = _payments_main_panel_html(ctx) - oobs = _post_header_html(ctx, oob=True) - oobs += _oob_header_html("post-header-child", "payments-header-child", - _payments_header_html(ctx)) - return oob_page(ctx, oobs_html=oobs, content_html=content) - - # =========================================================================== # POST / PUT / DELETE response components # =========================================================================== @@ -2221,15 +2181,6 @@ def render_calendar_description_edit(calendar) -> str: csrf=csrf, description=desc) -# --------------------------------------------------------------------------- -# Payments panel (public wrapper) -# --------------------------------------------------------------------------- - -def render_payments_panel(ctx: dict) -> str: - """Render the payments config panel for PUT response.""" - return _payments_main_panel_html(ctx) - - # --------------------------------------------------------------------------- # Calendars list panel (for POST create / DELETE) # --------------------------------------------------------------------------- @@ -2611,3 +2562,856 @@ def _cart_icon_oob(count: int) -> str: cart_href = cart_url_fn("/") if cart_url_fn else "/" return render("events-cart-icon-badge", cart_href=cart_href, count=str(count)) + + +# =========================================================================== +# SLOT PICKER JS — shared by entry edit + entry add forms +# =========================================================================== + +_SLOT_PICKER_JS = """\ +""" + + +# =========================================================================== +# Entry edit form +# =========================================================================== + +def _slot_options_html(day_slots, selected_slot_id=None) -> str: + """Build slot