Skip to main content

Milestone 7: Ticket list and detail

Purpose: Agents see ticket list with filters, open ticket detail with thread, reply and add internal notes, manage tags and assignee; real-time via Server-Sent Events (SSE).

Exit state: Ticket list (filters, sort, pagination); ticket detail (thread, reply composer with Tiptap, internal note); tags and assignee; reply sends via M06; SSE for real-time UI updates.

Spec reference: §2.4 Ticket Management, §5.5 Ticket, §5.6 Message, §5.13 Tag, §6.4–6.6 Layout and composer, §6.10 tickets pages, §4.11 Real-time (SSE), §10.2–10.3 Entity lifecycle (tickets, messages).

Prerequisites: M06 (Email pipeline — tickets exist from inbound or manual create).


7.1 Ticket list

Tasks

  1. tRPC list

    • Procedure: tenantId from context; filters: status, assignee_id, inbox_id, tag_ids (array), date_from, date_to; sort: created_at, updated_at, priority; order asc/desc; pagination: cursor or offset (page, limit). Return tickets with ticket_number, subject, inbox_id, status, priority, assignee_id, updated_at, first_response_at, resolved_at; include inbox name and assignee name/avatar for display.
    • Enforce role: Viewer can list; Agent and Admin can list and filter.
  2. List UI

    • Table or card list: columns/cards show subject, inbox badge, status badge, priority, assignee avatar, relative time (e.g. "2h ago"). Click row → navigate to /app/[slug]/tickets/[id].
    • Filter bar: status (multi or single), assignee (combobox), inbox (select), tags (multi-select), date range. Persist in URL query so shareable.
    • Sort: dropdown (newest, oldest, last activity, priority). Pagination: "Load more" or pages.
  3. Manual ticket creation (optional)

    • "New ticket" button: form (subject, inbox_id, optional priority, optional assignee). Create ticket with first message as internal note or empty; or create with no messages and allow first reply. Section 2.4 implies tickets can be created manually; API supports it (Section 2.8). Implement tRPC create and minimal modal/page.

Acceptance criteria

CriterionStatus
Ticket list loads with default sort (e.g. updated_at desc); filters and sort apply; pagination works.✅ Done
Agent can create ticket manually (subject, inbox); ticket appears in list.✅ Done
URL reflects filters so link can be shared.✅ Done
Ticket list stays in sync when tickets change (SSE: ticket.updated, ticket.message_added, etc. invalidate list refetch).Pending

7.2 Ticket detail and thread

Tasks

  1. tRPC get ticket + messages

    • Procedure: ticket by id (and tenantId); include inbox, assignee, tags (ticket_tags + tag names). Load messages by ticket_id order by created_at asc; exclude internal notes from "thread" if needed for display (or include with visual distinction). Return ticket + messages.
  2. Detail page

    • Route: /app/[slug]/tickets/[id]. Header: subject (editable inline), inbox name, status select, assignee select, priority select. Section 6.5.
    • Thread: scrollable list of message bubbles. Inbound vs outbound visually distinct; internal notes with "Internal" badge and different style. Each bubble: sender (name/email), date, body (HTML sanitized for inbound — DOMPurify), attachments (links to download via pre-signed URL). Section 6.6.
    • Inbound HTML: sanitize with DOMPurify before render; external images optional "Show images" toggle per spec.
  3. Reply composer

    • Tiptap editor: rich text (bold, italic, lists, blockquote, links, inline code); output HTML. Section 6.6. Tab or toggle: "Reply" (outbound email) vs "Internal note". Send button: on submit call tRPC (create message outbound or internal_note); for outbound, M06 mutation creates message and enqueues send job. Optimistic update or refetch after success.
    • Internal note: Tiptap Mention extension (or equivalent); typing @ triggers user picker (tenant members); selection stored in content; on submit M08 parses mention nodes and creates notifications for mentioned users (Section 2.10).
    • Empty body validation; show send error (e.g. from M06 send_status failed) and "Retry" on message row.
  4. Status and assignee inline

    • Status: select (open, in_progress, waiting, resolved, closed); save on change (tRPC update ticket). Assignee: combobox (tenant members); save on change; trigger assignment notification in M08.
    • First response: when first outbound is sent, set tickets.first_response_at. Resolved: when status set to resolved, set tickets.resolved_at (optional automation later).

Acceptance criteria

CriterionStatus
Opening ticket shows full thread; messages in order; internal notes distinguished; inbound HTML sanitized.✅ Done
Reply (email) and internal note both work; outbound triggers send job and shows sent/failed state.✅ Done
Status and assignee editable inline; changes persist; first_response_at and resolved_at set per spec.✅ Done
Internal note composer supports @mention (user picker); M08 creates notifications for mentioned users.Pending
Thread and header stay in sync when messages or ticket metadata change (SSE: ticket.updated, ticket.message_added trigger refetch).Pending

7.3 Tags

Tasks

  1. Tags CRUD

    • tRPC: list tags (tenant); create (name, color); update; delete. Unique (tenant_id, name). Section 5.13.
  2. Ticket tags

    • Add tag to ticket: tRPC add ticket_tag (ticket_id, tag_id); remove: delete ticket_tag. List tags on ticket in sidebar or header; tag input (multi-select or combobox) to add; remove by X on tag.
  3. Filter by tag

    • In ticket list filter bar, tag multi-select; filter tickets that have any of selected tags (ticket_tags join).

Acceptance criteria

CriterionStatus
Admin/Agent can create and edit tags; add/remove tags on ticket; filter list by tag.✅ Done
Tags are tenant-scoped; unique by name per tenant.✅ Done
Ticket tag changes reflected in real time for other viewers (SSE: ticket.updated).Pending

7.4 Assignee

Tasks

  1. Assignee select

    • Load tenant members (tenant_memberships + users); show in combobox. Update ticket assignee_id via tRPC. Enforce tenant membership.
  2. Notifications

    • On assign: create in-app notification and send email (ticket assigned) to new assignee. Implement in M08; from M07 only the tRPC update and event/call to M08 notification trigger are needed.

Acceptance criteria

CriterionStatus
Assignee can be set and changed; list shows assignee; filter by assignee works.✅ Done
Assignment triggers notification (M08); M07 only needs to call or emit event.✅ Done
Assignee changes reflected in list and detail in real time (SSE: ticket.assigned / ticket.updated).Pending

7.5 Real-time (Server-Sent Events)

Tasks

  1. SSE endpoint and subscription

    • Expose an authenticated SSE endpoint (e.g. /api/app/[slug]/events or equivalent) that streams events to the client for the current tenant. Section 4.11.
    • Client subscribes when the user is on a ticket-related view (list or detail); subscription is scoped to tenant (tenant_id from session). Connection authenticated via session cookie or bearer token.
  2. Event flow and client revalidation

    • Emit events when ticket or message data changes: e.g. ticket.updated, ticket.message_added, ticket.assigned, notification.new. Payloads are lightweight (e.g. { entity: "ticket", id, tenant_id } or { entity: "message", ticket_id, id }).
    • Client on receipt of an event: invalidate relevant React Query keys (e.g. tickets.list, tickets.getById for the affected ticket) so data refetches. Optional: refetchOnWindowFocus as fallback.
  3. Scalability and distribution

    • Use Redis Pub/Sub from day one for event distribution (Section 4.11): all publishers (web app after tRPC mutation, worker after DB write) publish to Redis; every app instance subscribes and forwards to its local SSE connections whose tenant matches. Same architecture for single- and multi-instance; no later migration when scaling.
    • SSE connection limits and rate limiting per tenant or per user to protect against abuse.
  4. Reconnect and fallback

    • Client implements reconnect with backoff on connection drop. Optional: fallback to short-interval polling (e.g. refetchInterval) when SSE is unavailable or after repeated reconnect failures.
  5. Redis and environment

    • Staging and production: Use Upstash Redis for pub/sub (SSE event distribution).
    • Local development: Add a Redis service to docker-compose so the web app and worker can connect to Redis without Upstash.
    • CI: Add Redis as a service (or dependency) in CI so that future E2E tests that exercise SSE or worker→Redis publishing can run. For now this is documented only; no CI change is required in M07.

Acceptance criteria

CriterionStatus
List and thread refresh when relevant SSE events are received; new messages appear in near real-time.Pending
SSE is the primary real-time mechanism; Redis Pub/Sub is used from day one for event distribution (horizontal scaling).Pending

7.6 Inbox disconnected banner

Tasks

  1. Banner in ticket view
    • When ticket’s inbox has connected_at null or last_error set, show banner above composer: "Replies are paused — inbox disconnected. [Reconnect ▸]" with link to inbox settings. Section 6.8.
    • Optionally disable send button when inbox disconnected.

Acceptance criteria

CriterionStatus
Banner shown when inbox is disconnected; Reconnect links to settings; send disabled or warned.Pending

7.7 Ticket delete and move to inbox

Tasks

  1. Ticket delete (Admin only)

    • tRPC: delete ticket (Admin role check). Hard delete: ticket + messages + ticket_tags; enqueue Inngest job or trigger async cleanup for S3 attachments (message attachments). Section 10.2. UI: "Delete ticket" in ticket header or dropdown; confirmation dialog ("This will permanently delete the ticket and all N messages. This cannot be undone."); on confirm call tRPC. Audit: ticket.deleted (M09).
  2. Move ticket to another inbox

    • tRPC: update ticket inbox_id (tenant-scoped; target inbox must belong to tenant). Section 10.2. UI: inbox dropdown or "Move to inbox" in header/command palette (M16); select target inbox; save. Thread (messages) stays with ticket; no message row changes. Audit: ticket.moved with metadata (from_inbox_id, to_inbox_id). M16 command palette "Move to inbox" invokes this.

Acceptance criteria

CriterionStatus
Admin can delete a ticket with confirmation; ticket and messages removed; S3 cleanup enqueued; audit ticket.deleted.Pending
Agent/Admin can move ticket to another tenant inbox; audit ticket.moved; command palette uses same tRPC.Pending

7.8 Bulk actions on ticket list

Tasks

  1. Multi-select and bulk actions
    • List UI: checkboxes or row selection; "Select all" (current page). Toolbar when selection non-empty: bulk change status, bulk assign (assignee), bulk add/remove tags, bulk move to inbox, bulk delete (Admin only). Section 10.2. Each action: confirmation where appropriate (e.g. bulk delete: "Permanently delete N tickets?"). tRPC: bulk update procedures (e.g. tickets.bulkUpdateStatus, tickets.bulkDelete) with tenant scope and role checks (bulk delete = Admin only).

Acceptance criteria

CriterionStatus
User can select multiple tickets; bulk change status, assignee, tags, move to inbox; bulk delete (Admin only) with confirmation.Pending

7.9 Message delete (Admin / GDPR)

Tasks

  1. Admin delete single message
    • tRPC: delete message (Admin only); hard delete message row and attachment refs; enqueue S3 cleanup for attachments. Section 10.3. UI: on message bubble (e.g. overflow menu) "Delete message" for Admins; confirmation ("This message will be permanently deleted."). Audit: message.deleted (M09). Ticket remains; if first message deleted, ticket subject preserved.

Acceptance criteria

CriterionStatus
Admin can delete a single message with confirmation; message and attachment refs removed; S3 cleanup enqueued; audit message.deleted.Pending

Milestone 7 sign-off

CriterionStatus
All tasks in 7.1–7.9 complete.Pending
All acceptance criteria met.Pending
Agents can list, filter, open tickets; reply and add internal notes; manage tags and assignee; ticket delete/move and bulk actions (Admin where required); see updates via SSE.Pending
E2E: Tickets critical path (list, open ticket, reply) — see INDEX — Testing strategy.Pending
Ready for M08 (Notifications and snippets).Pending