""" Orders service s-expression page components. Each function renders a complete page section (full page, OOB, or pagination) using shared s-expression components. Called from route handlers in place of ``render_template()``. """ from __future__ import annotations from typing import Any from shared.sexp.jinja_bridge import sexp, register_components from shared.sexp.helpers import ( call_url, get_asset_url, root_header_html, search_mobile_html, search_desktop_html, full_page, oob_page, ) from shared.sexp.page import HAMBURGER_HTML from shared.infrastructure.urls import market_product_url # --------------------------------------------------------------------------- # Service-specific component definitions # --------------------------------------------------------------------------- def load_orders_components() -> None: """Register orders-specific s-expression components (placeholder for future).""" pass # --------------------------------------------------------------------------- # Header helpers (shared auth + orders-specific) # --------------------------------------------------------------------------- def _auth_nav_html(ctx: dict) -> str: """Auth section desktop nav items.""" html = sexp( '(~nav-link :href h :label "newsletters" :select-colours sc)', h=call_url(ctx, "account_url", "/newsletters/"), sc=ctx.get("select_colours", ""), ) account_nav_html = ctx.get("account_nav_html", "") if account_nav_html: html += account_nav_html return html def _auth_header_html(ctx: dict, *, oob: bool = False) -> str: """Build the account section header row.""" return sexp( '(~menu-row :id "auth-row" :level 1 :colour "sky"' ' :link-href lh :link-label "account" :icon "fa-solid fa-user"' ' :nav-html nh :child-id "auth-header-child" :oob oob)', lh=call_url(ctx, "account_url", "/"), nh=_auth_nav_html(ctx), oob=oob, ) def _orders_header_html(ctx: dict, list_url: str) -> str: """Build the orders section header row.""" return sexp( '(~menu-row :id "orders-row" :level 2 :colour "sky"' ' :link-href lh :link-label "Orders" :icon "fa fa-gbp"' ' :child-id "orders-header-child")', lh=list_url, ) # --------------------------------------------------------------------------- # Orders list rendering # --------------------------------------------------------------------------- def _order_row_html(order: Any, detail_url: str) -> str: """Render a single order as desktop table row + mobile card.""" status = order.status or "pending" sl = status.lower() pill = ( "border-emerald-300 bg-emerald-50 text-emerald-700" if sl == "paid" else "border-rose-300 bg-rose-50 text-rose-700" if sl in ("failed", "cancelled") else "border-stone-300 bg-stone-50 text-stone-700" ) created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014" total = f"{order.currency or 'GBP'} {order.total_amount or 0:.2f}" return ( # Desktop row f'' f'#{order.id}' f'{created}' f'{order.description or ""}' f'{total}' f'{status}' f'View' # Mobile row f'
' f'
#{order.id}' f'{status}
' f'
{created}
' f'
{total}
' f'View
' ) def _orders_rows_html(orders: list, page: int, total_pages: int, url_for_fn: Any, qs_fn: Any) -> str: """Render order rows + infinite scroll sentinel.""" from shared.utils import route_prefix pfx = route_prefix() parts = [ _order_row_html(o, pfx + url_for_fn("orders.order.order_detail", order_id=o.id)) for o in orders ] if page < total_pages: next_url = pfx + url_for_fn("orders.list_orders") + qs_fn(page=page + 1) parts.append(sexp( '(~infinite-scroll :url u :page p :total-pages tp :id-prefix "orders" :colspan 5)', u=next_url, p=page, **{"total-pages": total_pages}, )) else: parts.append('End of results') return "".join(parts) def _orders_main_panel_html(orders: list, rows_html: str) -> str: """Main panel with table or empty state.""" if not orders: return ( '
' '
' 'No orders yet.
' ) return ( '
' '
' '' '' '' '' '' '' '' '' f'{rows_html}
OrderCreatedDescriptionTotalStatus
' ) def _orders_summary_html(ctx: dict) -> str: """Filter section for orders list.""" return ( '
' '

Recent orders placed via the checkout.

' f'
{search_mobile_html(ctx)}
' '
' ) # --------------------------------------------------------------------------- # Public API: orders list # --------------------------------------------------------------------------- async def render_orders_page(ctx: dict, orders: list, page: int, total_pages: int, search: str | None, search_count: int, url_for_fn: Any, qs_fn: Any) -> str: """Full page: orders list.""" from shared.utils import route_prefix ctx["search"] = search ctx["search_count"] = search_count list_url = route_prefix() + url_for_fn("orders.list_orders") rows = _orders_rows_html(orders, page, total_pages, url_for_fn, qs_fn) main = _orders_main_panel_html(orders, rows) hdr = root_header_html(ctx) hdr += sexp( '(div :id "root-header-child" :class "flex flex-col w-full items-center" (raw! a) (raw! o))', a=_auth_header_html(ctx), o=_orders_header_html(ctx, list_url), ) return full_page(ctx, header_rows_html=hdr, filter_html=_orders_summary_html(ctx), aside_html=search_desktop_html(ctx), content_html=main) async def render_orders_rows(ctx: dict, orders: list, page: int, total_pages: int, url_for_fn: Any, qs_fn: Any) -> str: """Pagination: just the table rows.""" return _orders_rows_html(orders, page, total_pages, url_for_fn, qs_fn) async def render_orders_oob(ctx: dict, orders: list, page: int, total_pages: int, search: str | None, search_count: int, url_for_fn: Any, qs_fn: Any) -> str: """OOB response for HTMX navigation to orders list.""" from shared.utils import route_prefix ctx["search"] = search ctx["search_count"] = search_count list_url = route_prefix() + url_for_fn("orders.list_orders") rows = _orders_rows_html(orders, page, total_pages, url_for_fn, qs_fn) main = _orders_main_panel_html(orders, rows) oobs = ( _auth_header_html(ctx, oob=True) + sexp( '(div :id "auth-header-child" :hx-swap-oob "outerHTML"' ' :class "flex flex-col w-full items-center" (raw! o))', o=_orders_header_html(ctx, list_url), ) + root_header_html(ctx, oob=True) ) return oob_page(ctx, oobs_html=oobs, filter_html=_orders_summary_html(ctx), aside_html=search_desktop_html(ctx), content_html=main) # --------------------------------------------------------------------------- # Single order detail # --------------------------------------------------------------------------- def _order_items_html(order: Any) -> str: """Render order items list.""" if not order or not order.items: return "" items = [] for item in order.items: prod_url = market_product_url(item.product_slug) img = ( f'{item.product_title or ' if item.product_image else '
No image
' ) items.append( f'
  • ' f'
    {img}
    ' f'
    ' f'

    {item.product_title or "Unknown product"}

    ' f'

    Product ID: {item.product_id}

    ' f'

    Qty: {item.quantity}

    ' f'

    {item.currency or order.currency or "GBP"} {item.unit_price or 0:.2f}

    ' f'
  • ' ) return ( '
    ' '

    Items

    ' f'
    ' ) def _calendar_items_html(calendar_entries: list | None) -> str: """Render calendar bookings for an order.""" if not calendar_entries: return "" items = [] for e in calendar_entries: st = e.state or "" pill = ( "bg-emerald-100 text-emerald-800" if st == "confirmed" else "bg-amber-100 text-amber-800" if st == "provisional" else "bg-blue-100 text-blue-800" if st == "ordered" else "bg-stone-100 text-stone-700" ) ds = e.start_at.strftime("%-d %b %Y, %H:%M") if e.start_at else "" if e.end_at: ds += f" \u2013 {e.end_at.strftime('%-d %b %Y, %H:%M')}" items.append( f'
  • ' f'
    {e.name}' f'' f'{st.capitalize()}
    ' f'
    {ds}
    ' f'
    \u00a3{e.cost or 0:.2f}
  • ' ) return ( '
    ' '

    Calendar bookings in this order

    ' f'
    ' ) def _order_main_html(order: Any, calendar_entries: list | None) -> str: """Main panel for single order detail.""" summary = sexp( '(~order-summary-card :order-id oid :created-at ca :description d :status s :currency c :total-amount ta)', oid=order.id, ca=order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else None, d=order.description, s=order.status, c=order.currency, ta=f"{order.total_amount:.2f}" if order.total_amount else None, ) return f'
    {summary}{_order_items_html(order)}{_calendar_items_html(calendar_entries)}
    ' def _order_filter_html(order: Any, list_url: str, recheck_url: str, pay_url: str, csrf_token: str) -> str: """Filter section for single order detail.""" created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014" status = order.status or "pending" pay = ( f'' f'Open payment page' ) if status != "paid" else "" return ( '
    ' f'

    Placed {created} · Status: {status}

    ' '
    ' f'All orders' f'
    ' f'
    ' f'{pay}
    ' ) async def render_order_page(ctx: dict, order: Any, calendar_entries: list | None, url_for_fn: Any) -> str: """Full page: single order detail.""" from shared.utils import route_prefix from shared.browser.app.csrf import generate_csrf_token pfx = route_prefix() detail_url = pfx + url_for_fn("orders.order.order_detail", order_id=order.id) list_url = pfx + url_for_fn("orders.list_orders") recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id) pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id) main = _order_main_html(order, calendar_entries) filt = _order_filter_html(order, list_url, recheck_url, pay_url, generate_csrf_token()) # Header stack: root -> auth -> orders -> order hdr = root_header_html(ctx) order_row = sexp( '(~menu-row :id "order-row" :level 3 :colour "sky" :link-href lh :link-label "Order" :icon "fa fa-gbp")', lh=detail_url, ) hdr += sexp( '(div :id "root-header-child" :class "flex flex-col w-full items-center" (raw! a)' ' (div :id "auth-header-child" :class "flex flex-col w-full items-center" (raw! b)' ' (div :id "orders-header-child" :class "flex flex-col w-full items-center" (raw! c))))', a=_auth_header_html(ctx), b=_orders_header_html(ctx, list_url), c=order_row, ) return full_page(ctx, header_rows_html=hdr, filter_html=filt, content_html=main) async def render_order_oob(ctx: dict, order: Any, calendar_entries: list | None, url_for_fn: Any) -> str: """OOB response for single order detail.""" from shared.utils import route_prefix from shared.browser.app.csrf import generate_csrf_token pfx = route_prefix() detail_url = pfx + url_for_fn("orders.order.order_detail", order_id=order.id) list_url = pfx + url_for_fn("orders.list_orders") recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id) pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id) main = _order_main_html(order, calendar_entries) filt = _order_filter_html(order, list_url, recheck_url, pay_url, generate_csrf_token()) order_row_oob = sexp( '(~menu-row :id "order-row" :level 3 :colour "sky" :link-href lh :link-label "Order" :icon "fa fa-gbp" :oob true)', lh=detail_url, ) oobs = ( sexp('(div :id "orders-header-child" :hx-swap-oob "outerHTML" :class "flex flex-col w-full items-center" (raw! o))', o=order_row_oob) + root_header_html(ctx, oob=True) ) return oob_page(ctx, oobs_html=oobs, filter_html=filt, content_html=main)