# Ticket UX Improvements: +/- Buttons, Sold Count, Cart Grouping ## Context The entry page currently uses a numeric input + "Buy Tickets" button, which replaces itself with a confirmation after purchase. The cart lists each ticket individually. The user wants the ticket UX to match the product pattern: +/- buttons, "in basket" count, tickets grouped by event on cart. ## Requirements 1. **Entry page**: Show tickets sold count + current user's "in basket" count 2. **Entry page**: Replace qty input with "Add to basket" / +/- buttons (product pattern) 3. **Entry page**: Keep form active after adding (don't replace with confirmation) 4. **Cart page**: Group tickets by event (entry_id + ticket_type), show quantity with +/- buttons --- ## 1. Add `ticket_type_id` to TicketDTO **File**: `shared/contracts/dtos.py` - Add `ticket_type_id: int | None = None` field to `TicketDTO` **File**: `shared/services/calendar_impl.py` - In `_ticket_to_dto()`, populate `ticket_type_id=ticket.ticket_type_id` **Sync**: Copy to all 4 app submodule copies. ## 2. New ticket service functions **File**: `events/bp/tickets/services/tickets.py` - Add `get_user_reserved_count(session, entry_id, user_id, session_id, ticket_type_id=None) -> int` - Counts reserved tickets for this user+entry+type - Add `get_sold_ticket_count(session, entry_id) -> int` - Counts all non-cancelled tickets for this entry - Add `cancel_latest_reserved_ticket(session, entry_id, user_id, session_id, ticket_type_id=None) -> bool` - Finds the most recently created reserved ticket for this user+entry+type, sets state='cancelled'. Returns True if one was cancelled. ## 3. Add `adjust_quantity` route to events tickets blueprint **File**: `events/bp/tickets/routes.py` - New route: `POST /tickets/adjust/` - Form fields: `entry_id`, `ticket_type_id` (optional), `count` (target quantity) - Logic: - Get current user reserved count for this entry/type - If count > current: create `(count - current)` tickets via `create_ticket()` - If count < current: cancel `(current - count)` tickets via `cancel_latest_reserved_ticket()` in a loop - If count == 0: cancel all - Check availability before adding (like existing `buy_tickets`) - Response: re-render `_buy_form.html` (HTMX swap replaces form, keeps it active) - Include OOB cart-mini update: `{{ mini(oob='true') }}` ## 4. Inject ticket counts into entry page context **File**: `events/bp/calendar_entry/routes.py` — `inject_root` context processor - Add `ticket_sold_count`: total non-cancelled tickets for entry (via `get_sold_ticket_count`) - Add `user_ticket_count`: current user's reserved count (via `get_user_reserved_count`) - For multi-type entries, add `user_ticket_counts_by_type`: dict mapping ticket_type_id → count ## 5. Rewrite entry page buy form **File**: `events/templates/_types/tickets/_buy_form.html` - Show "X sold" (from `ticket_sold_count`) alongside "X remaining" - Show "X in basket" for current user **For single-price entries (no ticket types)**: - If `user_ticket_count == 0`: show "Add to basket" button (posts to `/tickets/adjust/` with count=1) - If `user_ticket_count > 0`: show `[-]` [count badge] `[+]` buttons - Minus: posts count=user_ticket_count-1 - Plus: posts count=user_ticket_count+1 - All forms: `hx-post`, `hx-target="#ticket-buy-{{ entry.id }}"`, `hx-swap="outerHTML"` **For multi-type entries**: - Same pattern per ticket type row, using `user_ticket_counts_by_type[tt.id]` Style: match product pattern exactly — emerald circular buttons, w-8 h-8, cart icon with badge. ## 6. Add ticket quantity route to cart app **File**: `cart/bp/cart/global_routes.py` - New route: `POST /cart/ticket-quantity/` - Form fields: `entry_id`, `ticket_type_id` (optional), `count` (target quantity) - Logic: call into CalendarService or directly use ticket functions - Since cart app uses service contracts, add `adjust_ticket_quantity` to CalendarService protocol **File**: `shared/contracts/protocols.py` — CalendarService - Add: `adjust_ticket_quantity(session, entry_id, count, *, user_id, session_id, ticket_type_id=None) -> int` **File**: `shared/services/calendar_impl.py` - Implement `adjust_ticket_quantity`: - Same logic as events adjust route (create/cancel to match target count) - Return new count **File**: `shared/services/stubs.py` - Add stub: returns 0 Response: `HX-Refresh: true` (same as product quantity route). ## 7. Cart page: group tickets by event with +/- buttons **File**: `cart/templates/_types/cart/_cart.html` — ticket section (lines 63-95) - Replace individual ticket list with grouped display - Group `ticket_cart_entries` by `(entry_id, ticket_type_id)`: - Use Jinja `groupby` on `entry_id` first, then sub-group by `ticket_type_name` - Or pre-group in the route handler and pass as a dict **Approach**: Pre-group in the route handler for cleaner templates. **File**: `cart/bp/cart/page_routes.py` — `page_view` - After getting `page_tickets`, group them into a list of dicts: ``` [{"entry_name": ..., "entry_id": ..., "ticket_type_name": ..., "ticket_type_id": ..., "entry_start_at": ..., "entry_end_at": ..., "price": ..., "quantity": N}] ``` - Pass as `ticket_groups` to template **File**: `cart/bp/cart/global_routes.py` — overview/checkout routes - Same grouping for global cart view if tickets appear there **Cart ticket group template**: Each group shows: - Event name + ticket type (if any) - Date/time - Price per ticket - `-` [qty] `+` buttons (posting to `/cart/ticket-quantity/`) - Line total (price × qty) Match product `cart_item` macro style (article card with quantity controls). ## 8. Cart summary update **File**: `cart/templates/_types/cart/_cart.html` — `summary` macro - Update Items count: include ticket quantities in total (currently just product quantities) ## Files to modify (summary) - `shared/contracts/dtos.py` — add ticket_type_id to TicketDTO - `shared/contracts/protocols.py` — add adjust_ticket_quantity to CalendarService - `shared/services/calendar_impl.py` — implement adjust_ticket_quantity, update _ticket_to_dto - `shared/services/stubs.py` — add stub - `events/bp/tickets/services/tickets.py` — add count/cancel functions - `events/bp/tickets/routes.py` — add adjust route - `events/bp/calendar_entry/routes.py` — inject sold/user counts - `events/templates/_types/tickets/_buy_form.html` — rewrite with +/- pattern - `cart/bp/cart/global_routes.py` — add ticket-quantity route - `cart/bp/cart/page_routes.py` — group tickets - `cart/templates/_types/cart/_cart.html` — grouped ticket display with +/- - All 4 app `shared/` submodule copies synced ## Verification 1. Visit entry page → see "X sold", "X in basket", "Add to basket" button 2. Click "Add to basket" → form stays, shows `-` [1] `+`, basket count shows "1 in basket" 3. Click `+` → count increases, sold count increases 4. Click `-` → count decreases, ticket cancelled 5. Visit cart page → tickets grouped by event, +/- buttons work 6. Checkout flow still works (existing tests)