""" Ticket admin blueprint — check-in interface and ticket management. Routes: GET /admin/tickets/ — Ticket dashboard (scan + list) GET /admin/tickets/entry// — Tickets for a specific entry POST /admin/tickets//checkin — Check in a ticket GET /admin/tickets// — Ticket admin detail """ from __future__ import annotations import logging from quart import ( Blueprint, g, request, render_template, make_response, jsonify, ) from sqlalchemy import select, func from sqlalchemy.orm import selectinload from models.calendars import CalendarEntry, Ticket, TicketType from shared.browser.app.authz import require_admin from shared.browser.app.redis_cacher import clear_cache from shared.sx.helpers import sx_response from ..tickets.services.tickets import ( get_ticket_by_code, get_tickets_for_entry, checkin_ticket, ) logger = logging.getLogger(__name__) def register() -> Blueprint: bp = Blueprint("ticket_admin", __name__, url_prefix="/admin/tickets") @bp.get("/") @require_admin async def dashboard(): """Ticket admin dashboard with QR scanner and recent tickets.""" from shared.browser.app.utils.htmx import is_htmx_request # Get recent tickets result = await g.s.execute( select(Ticket) .options( selectinload(Ticket.entry).selectinload(CalendarEntry.calendar), selectinload(Ticket.ticket_type), ) .order_by(Ticket.created_at.desc()) .limit(50) ) tickets = result.scalars().all() # Stats total = await g.s.scalar(select(func.count(Ticket.id))) confirmed = await g.s.scalar( select(func.count(Ticket.id)).where(Ticket.state == "confirmed") ) checked_in = await g.s.scalar( select(func.count(Ticket.id)).where(Ticket.state == "checked_in") ) reserved = await g.s.scalar( select(func.count(Ticket.id)).where(Ticket.state == "reserved") ) stats = { "total": total or 0, "confirmed": confirmed or 0, "checked_in": checked_in or 0, "reserved": reserved or 0, } from shared.sx.page import get_template_context from sx.sx_components import render_ticket_admin_page, render_ticket_admin_oob ctx = await get_template_context() if not is_htmx_request(): html = await render_ticket_admin_page(ctx, tickets, stats) return await make_response(html, 200) else: sx_src = await render_ticket_admin_oob(ctx, tickets, stats) return sx_response(sx_src) @bp.get("/entry//") @require_admin async def entry_tickets(entry_id: int): """List all tickets for a specific calendar entry.""" from shared.browser.app.utils.htmx import is_htmx_request entry = await g.s.scalar( select(CalendarEntry) .where( CalendarEntry.id == entry_id, CalendarEntry.deleted_at.is_(None), ) .options(selectinload(CalendarEntry.calendar)) ) if not entry: return await make_response("Entry not found", 404) tickets = await get_tickets_for_entry(g.s, entry_id) from sx.sx_components import render_entry_tickets_admin html = render_entry_tickets_admin(entry, tickets) return sx_response(html) @bp.get("/lookup/") @require_admin async def lookup(): """Look up a ticket by code (used by scanner).""" code = request.args.get("code", "").strip() if not code: return await make_response( '
Enter a ticket code
', 200, ) ticket = await get_ticket_by_code(g.s, code) from sx.sx_components import render_lookup_result if not ticket: return sx_response(render_lookup_result(None, "Ticket not found")) return sx_response(render_lookup_result(ticket, None)) @bp.post("//checkin/") @require_admin @clear_cache(tag="calendars", tag_scope="all") async def do_checkin(code: str): """Check in a ticket by its code.""" success, error = await checkin_ticket(g.s, code) from sx.sx_components import render_checkin_result if not success: return sx_response(render_checkin_result(False, error, None)) ticket = await get_ticket_by_code(g.s, code) return sx_response(render_checkin_result(True, None, ticket)) return bp