# 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)
