"""Ticket panels, forms, admin views, buy/adjust controls.""" from __future__ import annotations from markupsafe import escape from shared.sx.helpers import sx_call from shared.sx.parser import SxExpr from .utils import _list_container, _cart_icon_ctx # --------------------------------------------------------------------------- # Ticket widget (inline +/- for entry cards) # --------------------------------------------------------------------------- def _ticket_widget_data(entry, qty: int, ticket_url: str) -> dict: """Extract ticket widget data for sx composition.""" from shared.browser.app.csrf import generate_csrf_token eid = entry.id tp = getattr(entry, "ticket_price", 0) or 0 return { "entry_id": str(eid), "price": f"\u00a3{tp:.2f}", "qty": qty, "ticket_url": ticket_url, "csrf": generate_csrf_token(), } # --------------------------------------------------------------------------- # Tickets main panel (my tickets) # --------------------------------------------------------------------------- def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: """Render my tickets list via data extraction + sx defcomp.""" from quart import url_for ticket_data = [] if tickets: for ticket in tickets: entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) cal = getattr(entry, "calendar", None) if entry else None time_str = "" if entry and entry.start_at: time_str = entry.start_at.strftime("%A, %B %d, %Y at %H:%M") if entry.end_at: time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}" ticket_data.append({ "href": url_for("defpage_ticket_detail", code=ticket.code), "entry-name": entry.name if entry else "Unknown event", "type-name": tt.name if tt else None, "time-str": time_str or None, "cal-name": cal.name if cal else None, "state": getattr(ticket, "state", ""), "code-prefix": ticket.code[:8], }) return sx_call("events-tickets-panel-from-data", list_container=_list_container(ctx), tickets=ticket_data or None) # --------------------------------------------------------------------------- # Ticket detail panel # --------------------------------------------------------------------------- def _ticket_detail_panel_html(ctx: dict, ticket) -> str: """Render a single ticket detail with QR code via data + sx defcomp.""" from quart import url_for entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code cal = getattr(entry, "calendar", None) if entry else None checked_in_at = getattr(ticket, "checked_in_at", None) bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"} time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None if time_range and entry.end_at: time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}" qr_script = ( f"(function(){{var c=document.getElementById('ticket-qr-{code}');" "if(c&&typeof QRCode!=='undefined'){" "var cv=document.createElement('canvas');" f"QRCode.toCanvas(cv,'{code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});" "}})()" ) return sx_call("events-ticket-detail-from-data", list_container=_list_container(ctx), back_href=url_for("defpage_my_tickets"), header_bg=bg_map.get(state, "bg-stone-50"), entry_name=entry.name if entry else "Ticket", state=state, type_name=tt.name if tt else None, code=code, time_date=time_date, time_range=time_range, cal_name=cal.name if cal else None, type_desc=f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None, checkin_str=checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None, qr_script=qr_script) # --------------------------------------------------------------------------- # Ticket admin main panel # --------------------------------------------------------------------------- def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: """Render ticket admin dashboard via data extraction + sx defcomp.""" from quart import url_for csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") ticket_data = [] for ticket in tickets: entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code checked_in_at = getattr(ticket, "checked_in_at", None) ticket_data.append({ "code": code, "code-short": code[:12] + "...", "entry-name": entry.name if entry else "\u2014", "date-str": entry.start_at.strftime("%d %b %Y, %H:%M") if entry and entry.start_at else None, "type-name": tt.name if tt else "\u2014", "state": state, "checkin-url": url_for("ticket_admin.do_checkin", code=code) if state in ("confirmed", "reserved") else None, "csrf": csrf, "checked-in-time": checked_in_at.strftime("%H:%M") if checked_in_at else None, }) return sx_call("events-ticket-admin-panel-from-data", list_container=_list_container(ctx), lookup_url=url_for("ticket_admin.lookup"), tickets=ticket_data or None, total=stats.get("total", 0), confirmed=stats.get("confirmed", 0), checked_in=stats.get("checked_in", 0), reserved=stats.get("reserved", 0)) # --------------------------------------------------------------------------- # Public render: ticket widget # --------------------------------------------------------------------------- def render_ticket_widget(entry, qty: int, ticket_url: str) -> str: """Render the +/- ticket widget for page_summary / all_events adjust_ticket.""" data = _ticket_widget_data(entry, qty, ticket_url) return sx_call("events-tw-widget-from-data", **data) # --------------------------------------------------------------------------- # Ticket admin: checkin result # --------------------------------------------------------------------------- def render_checkin_result(success: bool, error: str | None, ticket) -> str: """Render checkin result: table row on success, error div on failure.""" if not success: return sx_call("events-checkin-error", message=error or "Check-in failed") if not ticket: return "" code = ticket.code entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) checked_in_at = getattr(ticket, "checked_in_at", None) return sx_call("events-checkin-success-row-from-data", code=code, code_short=code[:12] + "...", entry_name=entry.name if entry else "\u2014", date_str=entry.start_at.strftime("%d %b %Y, %H:%M") if entry and entry.start_at else None, type_name=tt.name if tt else "\u2014", time_str=checked_in_at.strftime("%H:%M") if checked_in_at else "Just now") # --------------------------------------------------------------------------- # Ticket admin: lookup result # --------------------------------------------------------------------------- def render_lookup_result(ticket, error: str | None) -> str: """Render ticket lookup result via data extraction + sx defcomp.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token if error: return sx_call("events-lookup-error", message=error) if not ticket: return "" entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code checked_in_at = getattr(ticket, "checked_in_at", None) cal = getattr(entry, "calendar", None) if entry else None checkin_url = None if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) return sx_call("events-lookup-result-from-data", entry_name=entry.name if entry else "Unknown event", type_name=tt.name if tt else None, date_str=entry.start_at.strftime("%A, %B %d, %Y at %H:%M") if entry and entry.start_at else None, cal_name=cal.name if cal else None, state=state, code=code, checked_in_str=checked_in_at.strftime("%B %d, %Y at %H:%M") if checked_in_at else None, checkin_url=checkin_url, csrf=generate_csrf_token()) # --------------------------------------------------------------------------- # Ticket admin: entry tickets table # --------------------------------------------------------------------------- def render_entry_tickets_admin(entry, tickets: list) -> str: """Render admin ticket table via data extraction + sx defcomp.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() count = len(tickets) suffix = "s" if count != 1 else "" ticket_data = [] for ticket in tickets: tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code checked_in_at = getattr(ticket, "checked_in_at", None) checkin_url = None if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) ticket_data.append({ "code": code, "code-short": code[:12] + "...", "type-name": tt.name if tt else "\u2014", "state": state, "checkin-url": checkin_url, "checked-in-time": checked_in_at.strftime("%H:%M") if checked_in_at else None, }) return sx_call("events-entry-tickets-admin-from-data", entry_name=entry.name, count_label=f"{count} ticket{suffix}", tickets=ticket_data or None, csrf=csrf) # --------------------------------------------------------------------------- # Ticket type main panel # --------------------------------------------------------------------------- def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year, *, oob: bool = False) -> str: """Render ticket type detail view.""" from quart import url_for, g styles = getattr(g, "styles", None) or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") cal_slug = getattr(calendar, "slug", "") cost = getattr(ticket_type, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" count = getattr(ticket_type, "count", 0) tid = str(ticket_type.id) edit_url = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit", ticket_type_id=ticket_type.id, calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=entry.id, ) def _col(label, val): return sx_call("events-ticket-type-col", label=label, value=val) return sx_call("events-ticket-type-panel", ticket_id=tid, list_container=list_container, c1=_col("Name", ticket_type.name), c2=_col("Cost", cost_str), c3=_col("Count", str(count)), pre_action=pre_action, edit_url=edit_url) # --------------------------------------------------------------------------- # Ticket types table # --------------------------------------------------------------------------- def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -> str: """Render ticket types table via data extraction + sx defcomp.""" from quart import url_for, g from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() styles = getattr(g, "styles", None) or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "") hx_select = getattr(g, "hx_select_search", "#main-panel") cal_slug = getattr(calendar, "slug", "") eid = entry.id types_data = [] if ticket_types: for tt in ticket_types: cost = getattr(tt, "cost", None) types_data.append({ "tt-href": url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get", calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=eid, ticket_type_id=tt.id, ), "tt-name": tt.name, "cost-str": f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00", "count": str(tt.count), "del-url": url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete", calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=eid, ticket_type_id=tt.id, ), }) add_url = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.add_form", calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day, ) return sx_call("events-ticket-types-table-from-data", list_container=list_container, ticket_types=types_data or None, action_btn=action_btn, add_url=add_url, tr_cls=tr_cls, pill_cls=pill_cls, hx_select=hx_select, csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') # --------------------------------------------------------------------------- # Buy result (ticket purchase confirmation) # --------------------------------------------------------------------------- def render_buy_result(entry, created_tickets, remaining, cart_count) -> str: """Render buy result card with OOB cart icon — single response component.""" from quart import url_for tickets = [ {"href": url_for("defpage_ticket_detail", code=t.code), "code_short": t.code[:12] + "..."} for t in created_tickets ] cart_ctx = _cart_icon_ctx(cart_count) return sx_call("events-buy-response", entry_id=str(entry.id), tickets=tickets, remaining=remaining, my_tickets_href=url_for("defpage_my_tickets"), **cart_ctx) # --------------------------------------------------------------------------- # Buy form (ticket +/- controls) # --------------------------------------------------------------------------- def render_buy_form(entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type) -> str: """Render the ticket buy/adjust form — data only, .sx does layout.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token tp = getattr(entry, "ticket_price", None) if tp is None: return "" ticket_types_orm = getattr(entry, "ticket_types", None) or [] active_types = [tt for tt in ticket_types_orm if getattr(tt, "deleted_at", None) is None] types_data = [ {"id": tt.id, "name": tt.name, "cost_str": f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00"} for tt in active_types ] # String keys so .sx can look up via (get counts (str id)) counts_by_type = {} if user_ticket_counts_by_type: counts_by_type = {str(k): v for k, v in user_ticket_counts_by_type.items()} return sx_call("events-buy-form", entry_id=entry.id, state=getattr(entry, "state", ""), price_str=f"\u00a3{tp:.2f}", adjust_url=url_for("tickets.adjust_quantity"), csrf=generate_csrf_token(), my_tickets_href=url_for("defpage_my_tickets"), info_sold=ticket_sold_count or None, info_remaining=ticket_remaining, info_basket=user_ticket_count or None, ticket_types=types_data if types_data else None, user_ticket_counts_by_type=counts_by_type if counts_by_type else None, user_ticket_count=user_ticket_count or 0) # --------------------------------------------------------------------------- # Adjust response (OOB cart icon + buy form) # --------------------------------------------------------------------------- def render_adjust_response(entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type, cart_count) -> str: """Render ticket adjust response — single response component with OOB cart.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token tp = getattr(entry, "ticket_price", None) if tp is None: return "" ticket_types_orm = getattr(entry, "ticket_types", None) or [] active_types = [tt for tt in ticket_types_orm if getattr(tt, "deleted_at", None) is None] types_data = [ {"id": tt.id, "name": tt.name, "cost_str": f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00"} for tt in active_types ] # String keys so .sx can look up via (get counts (str id)) counts_by_type = {} if user_ticket_counts_by_type: counts_by_type = {str(k): v for k, v in user_ticket_counts_by_type.items()} cart_ctx = _cart_icon_ctx(cart_count) return sx_call("events-adjust-response", entry_id=entry.id, state=getattr(entry, "state", ""), price_str=f"\u00a3{tp:.2f}", adjust_url=url_for("tickets.adjust_quantity"), csrf=generate_csrf_token(), my_tickets_href=url_for("defpage_my_tickets"), info_sold=ticket_sold_count or None, info_remaining=ticket_remaining, info_basket=user_ticket_count or None, ticket_types=types_data if types_data else None, user_ticket_counts_by_type=counts_by_type if counts_by_type else None, user_ticket_count=user_ticket_count or 0, **cart_ctx) # --------------------------------------------------------------------------- # Ticket types header rows # --------------------------------------------------------------------------- def _ticket_types_header_html(ctx: dict, *, oob: bool = False) -> str: """Build the ticket types header row.""" from quart import url_for calendar = ctx.get("calendar") entry = ctx.get("entry") if not calendar or not entry: return "" cal_slug = getattr(calendar, "slug", "") day = ctx.get("day") month = ctx.get("month") year = ctx.get("year") link_href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get", calendar_slug=cal_slug, entry_id=entry.id, year=year, month=month, day=day) label_html = '