"""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 ( _ticket_state_badge_html, _list_container, _cart_icon_oob, ) # --------------------------------------------------------------------------- # Ticket widget (inline +/- for entry cards) # --------------------------------------------------------------------------- def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str: """Render the inline +/- ticket widget.""" csrf_token_val = "" if ctx: ct = ctx.get("csrf_token") csrf_token_val = ct() if callable(ct) else (ct or "") else: try: from flask_wtf.csrf import generate_csrf csrf_token_val = generate_csrf() except Exception: pass eid = entry.id tp = getattr(entry, "ticket_price", 0) or 0 tgt = f"#page-ticket-{eid}" def _tw_form(count_val, btn_html): return sx_call("events-tw-form", ticket_url=ticket_url, target=tgt, csrf=csrf_token_val, entry_id=str(eid), count_val=str(count_val), btn=btn_html) if qty == 0: inner = _tw_form(1, sx_call("events-tw-cart-plus")) else: minus = _tw_form(qty - 1, sx_call("events-tw-minus")) cart_icon = sx_call("events-tw-cart-icon", qty=str(qty)) plus = _tw_form(qty + 1, sx_call("events-tw-plus")) inner = minus + cart_icon + plus return sx_call("events-tw-widget", entry_id=str(eid), price=f"\u00a3{tp:.2f}", inner=SxExpr(inner)) # --------------------------------------------------------------------------- # Tickets main panel (my tickets) # --------------------------------------------------------------------------- def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: """Render my tickets list.""" from quart import url_for ticket_cards = [] if tickets: for ticket in tickets: href = url_for("defpage_ticket_detail", code=ticket.code) entry = getattr(ticket, "entry", None) entry_name = entry.name if entry else "Unknown event" tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") 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_cards.append(sx_call("events-ticket-card", href=href, entry_name=entry_name, type_name=tt.name if tt else None, time_str=time_str or None, cal_name=cal.name if cal else None, badge=_ticket_state_badge_html(state), code_prefix=ticket.code[:8])) cards_html = "".join(ticket_cards) return sx_call("events-tickets-panel", list_container=_list_container(ctx), has_tickets=bool(tickets), cards=SxExpr(cards_html)) # --------------------------------------------------------------------------- # Ticket detail panel # --------------------------------------------------------------------------- def _ticket_detail_panel_html(ctx: dict, ticket) -> str: """Render a single ticket detail with QR code.""" 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"} header_bg = bg_map.get(state, "bg-stone-50") entry_name = entry.name if entry else "Ticket" back_href = url_for("defpage_my_tickets") # Badge with larger sizing badge = (_ticket_state_badge_html(state)).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm') # Time info 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')}" tt_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 = ( 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", list_container=_list_container(ctx), back_href=back_href, header_bg=header_bg, entry_name=entry_name, badge=SxExpr(badge), 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=tt_desc, checkin_str=checkin_str, 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.""" from quart import url_for csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") lookup_url = url_for("ticket_admin.lookup") # Stats cards stats_html = "" for label, key, border, bg, text_cls in [ ("Total", "total", "border-stone-200", "", "text-stone-900"), ("Confirmed", "confirmed", "border-emerald-200", "bg-emerald-50", "text-emerald-700"), ("Checked In", "checked_in", "border-blue-200", "bg-blue-50", "text-blue-700"), ("Reserved", "reserved", "border-amber-200", "bg-amber-50", "text-amber-700"), ]: val = stats.get(key, 0) lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500" stats_html += sx_call("events-ticket-admin-stat", border=border, bg=bg, text_cls=text_cls, label_cls=lbl_cls, value=str(val), label=label) # Ticket rows rows_html = "" for ticket in tickets: entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code date_html = "" if entry and entry.start_at: date_html = sx_call("events-ticket-admin-date", date_str=entry.start_at.strftime("%d %b %Y, %H:%M")) action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) action_html = sx_call("events-ticket-admin-checkin-form", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": checked_in_at = getattr(ticket, "checked_in_at", None) t_str = checked_in_at.strftime("%H:%M") if checked_in_at else "" action_html = sx_call("events-ticket-admin-checked-in", time_str=t_str) rows_html += sx_call("events-ticket-admin-row", code=code, code_short=code[:12] + "...", entry_name=entry.name if entry else "\u2014", date=SxExpr(date_html), type_name=tt.name if tt else "\u2014", badge=_ticket_state_badge_html(state), action=SxExpr(action_html)) return sx_call("events-ticket-admin-panel", list_container=_list_container(ctx), stats=SxExpr(stats_html), lookup_url=lookup_url, has_tickets=bool(tickets), rows=SxExpr(rows_html)) # --------------------------------------------------------------------------- # 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.""" return _ticket_widget_html(entry, qty, ticket_url, ctx={}) # --------------------------------------------------------------------------- # 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) time_str = checked_in_at.strftime("%H:%M") if checked_in_at else "Just now" date_html = "" if entry and entry.start_at: date_html = sx_call("events-ticket-admin-date", date_str=entry.start_at.strftime("%d %b %Y, %H:%M")) return sx_call("events-checkin-success-row", code=code, code_short=code[:12] + "...", entry_name=entry.name if entry else "\u2014", date=SxExpr(date_html), type_name=tt.name if tt else "\u2014", badge=_ticket_state_badge_html("checked_in"), time_str=time_str) # --------------------------------------------------------------------------- # Ticket admin: lookup result # --------------------------------------------------------------------------- def render_lookup_result(ticket, error: str | None) -> str: """Render ticket lookup result: error div or ticket info card.""" 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) csrf = generate_csrf_token() # Info section info_html = sx_call("events-lookup-info", entry_name=entry.name if entry else "Unknown event") if tt: info_html += sx_call("events-lookup-type", type_name=tt.name) if entry and entry.start_at: info_html += sx_call("events-lookup-date", date_str=entry.start_at.strftime("%A, %B %d, %Y at %H:%M")) cal = getattr(entry, "calendar", None) if entry else None if cal: info_html += sx_call("events-lookup-cal", cal_name=cal.name) info_html += sx_call("events-lookup-status", badge=_ticket_state_badge_html(state), code=code) if checked_in_at: info_html += sx_call("events-lookup-checkin-time", date_str=checked_in_at.strftime("%B %d, %Y at %H:%M")) # Action area action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) action_html = sx_call("events-lookup-checkin-btn", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": action_html = sx_call("events-lookup-checked-in") elif state == "cancelled": action_html = sx_call("events-lookup-cancelled") return sx_call("events-lookup-card", info=SxExpr(info_html), code=code, action=SxExpr(action_html)) # --------------------------------------------------------------------------- # Ticket admin: entry tickets table # --------------------------------------------------------------------------- def render_entry_tickets_admin(entry, tickets: list) -> str: """Render admin ticket table for a specific entry.""" 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 "" rows_html = "" 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) action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) action_html = sx_call("events-entry-tickets-admin-checkin", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": t_str = checked_in_at.strftime("%H:%M") if checked_in_at else "" action_html = sx_call("events-ticket-admin-checked-in", time_str=t_str) rows_html += sx_call("events-entry-tickets-admin-row", code=code, code_short=code[:12] + "...", type_name=tt.name if tt else "\u2014", badge=_ticket_state_badge_html(state), action=SxExpr(action_html)) if tickets: body_html = sx_call("events-entry-tickets-admin-table", rows=SxExpr(rows_html)) else: body_html = sx_call("events-entry-tickets-admin-empty") return sx_call("events-entry-tickets-admin-panel", entry_name=entry.name, count_label=f"{count} ticket{suffix}", body=body_html) # --------------------------------------------------------------------------- # 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 with rows and add button.""" 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 rows_html = "" if ticket_types: for tt in ticket_types: 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, ) 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, ) cost = getattr(tt, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" rows_html += sx_call("events-ticket-types-row", tr_cls=tr_cls, tt_href=tt_href, pill_cls=pill_cls, hx_select=hx_select, tt_name=tt.name, cost_str=cost_str, count=str(tt.count), action_btn=action_btn, del_url=del_url, csrf_hdr={"X-CSRFToken": csrf}) else: rows_html = sx_call("events-ticket-types-empty-row") 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", list_container=list_container, rows=SxExpr(rows_html), action_btn=action_btn, add_url=add_url) # --------------------------------------------------------------------------- # Buy result (ticket purchase confirmation) # --------------------------------------------------------------------------- def render_buy_result(entry, created_tickets, remaining, cart_count) -> str: """Render buy result card with created tickets + OOB cart icon.""" from quart import url_for cart_html = _cart_icon_oob(cart_count) count = len(created_tickets) suffix = "s" if count != 1 else "" tickets_html = "" for ticket in created_tickets: href = url_for("defpage_ticket_detail", code=ticket.code) tickets_html += sx_call("events-buy-result-ticket", href=href, code_short=ticket.code[:12] + "...") remaining_html = "" if remaining is not None: r_suffix = "s" if remaining != 1 else "" remaining_html = sx_call("events-buy-result-remaining", text=f"{remaining} ticket{r_suffix} remaining") my_href = url_for("defpage_my_tickets") return cart_html + sx_call("events-buy-result", entry_id=str(entry.id), count_label=f"{count} ticket{suffix} reserved", tickets=SxExpr(tickets_html), remaining=SxExpr(remaining_html), my_tickets_href=my_href) # --------------------------------------------------------------------------- # 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 with +/- controls.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() eid = entry.id eid_s = str(eid) tp = getattr(entry, "ticket_price", None) state = getattr(entry, "state", "") ticket_types = getattr(entry, "ticket_types", None) or [] if tp is None: return "" if state != "confirmed": return sx_call("events-buy-not-confirmed", entry_id=eid_s) adjust_url = url_for("tickets.adjust_quantity") target = f"#ticket-buy-{eid}" # Info line info_html = "" info_items = "" if ticket_sold_count: info_items += sx_call("events-buy-info-sold", count=str(ticket_sold_count)) if ticket_remaining is not None: info_items += sx_call("events-buy-info-remaining", count=str(ticket_remaining)) if user_ticket_count: info_items += sx_call("events-buy-info-basket", count=str(user_ticket_count)) if info_items: info_html = sx_call("events-buy-info-bar", items=SxExpr(info_items)) active_types = [tt for tt in ticket_types if getattr(tt, "deleted_at", None) is None] body_html = "" if active_types: type_items = "" for tt in active_types: type_count = user_ticket_counts_by_type.get(tt.id, 0) if user_ticket_counts_by_type else 0 cost_str = f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00" type_items += sx_call("events-buy-type-item", type_name=tt.name, cost_str=cost_str, adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id)) body_html = sx_call("events-buy-types-wrapper", items=SxExpr(type_items)) else: qty = user_ticket_count or 0 body_html = sx_call("events-buy-default", price_str=f"\u00a3{tp:.2f}", adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, qty)) return sx_call("events-buy-panel", entry_id=eid_s, info=SxExpr(info_html), body=body_html) def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket_type_id=None): """Render +/- ticket controls for buy form.""" from quart import url_for tt_html = sx_call("events-adjust-tt-hidden", ticket_type_id=str(ticket_type_id)) if ticket_type_id else "" eid_s = str(entry_id) def _adj_form(count_val, btn_html, *, extra_cls=""): return sx_call("events-adjust-form", adjust_url=adjust_url, target=target, extra_cls=extra_cls, csrf=csrf, entry_id=eid_s, tt=tt_html or None, count_val=str(count_val), btn=btn_html) if count == 0: return _adj_form(1, sx_call("events-adjust-cart-plus"), extra_cls="flex items-center") my_tickets_href = url_for("defpage_my_tickets") minus = _adj_form(count - 1, sx_call("events-adjust-minus")) cart_icon = sx_call("events-adjust-cart-icon", href=my_tickets_href, count=str(count)) plus = _adj_form(count + 1, sx_call("events-adjust-plus")) return sx_call("events-adjust-controls", minus=minus, cart_icon=cart_icon, plus=plus) # --------------------------------------------------------------------------- # 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: OOB cart icon + buy form.""" cart_html = _cart_icon_oob(cart_count) form_html = render_buy_form( entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type, ) return cart_html + form_html # --------------------------------------------------------------------------- # 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 = '