docs: fix PSA ticket management spec — pagination source, widget, linked ticket IDs

- Define PaginatedTicketResult provider type + parallel count fetch via CW /count endpoint
- Fix dashboard widget: updates existing TicketQueue (not new), uses searchTicketsQueue
- Fix NewTicketModal prefill: expand PSATicketInfo with company_id/board_id fields
- Correct Dashboard section description: not collapsible, TicketQueue already exists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 01:49:39 +00:00
parent 2b3d52ad77
commit c8b68ad26d

View File

@@ -63,10 +63,32 @@ Methods:
- `update_status(account_id, ticket_id, status_id) → PSATicketStatusUpdate`
- `list_resources(account_id, ticket_id) → list[PSAResource]`
### PSA Provider — New Abstract Methods
### PSA Provider — New Abstract Methods and Paginated Result Type
Four explicit methods added to `PSAProvider` base class and `ConnectWiseProvider`:
**New type in `backend/app/services/psa/types.py`:**
```python
@dataclass
class PaginatedTicketResult:
items: list[PSATicket]
total: int
page: int
page_size: int
```
**`search_tickets` signature change** — updated on both the abstract base and `ConnectWiseProvider` to return `PaginatedTicketResult` instead of `list[PSATicket]`:
```python
# base.py
@abstractmethod
async def search_tickets(self, query: str, **filters) -> PaginatedTicketResult: ...
```
**How `total` is fetched** — ConnectWise provides `GET /service/tickets/count?conditions=...` which accepts the same conditions string as the page fetch. The `ConnectWiseProvider.search_tickets()` implementation fires two parallel requests:
1. `GET /service/tickets?conditions=...&pageSize=N&page=N` — the current page
2. `GET /service/tickets/count?conditions=...` — returns `{ "count": 142 }`
Both use the same built conditions string. `asyncio.gather()` runs them in parallel. The count result is used to populate `PaginatedTicketResult.total`.
**New abstract methods** added to `PSAProvider` base and `ConnectWiseProvider`:
```python
async def list_resources(self, ticket_id: int) -> list[PSAResource]: ...
async def add_resource(self, ticket_id: int, member_id: int) -> PSAResource: ...
@@ -312,10 +334,28 @@ A "New Ticket" button in the ResolutionAssist session header. Always visible reg
### Both Paths — NewTicketModal Pre-population
When a session has a linked ticket, the modal receives:
**The linked ticket IDs problem:** The current `PSATicketInfo` type in `frontend/src/types/integrations.ts` only exposes `company_name` and `board_name` — not `company_id` or `board_id`. The modal needs the numeric IDs to pre-populate the form selects.
**Fix:** Expand `PSATicketInfo` in `types/integrations.ts` to add the optional ID fields:
```typescript
export interface PSATicketInfo {
id: string
summary: string
company_name: string | null
board_name: string | null
status_name: string | null
priority_name: string | null
company_id: number | null // add
board_id: number | null // add
}
```
These fields are already returned by the CW API in `get_ticket()` — update `_map_ticket()` in `ConnectWiseProvider` and the `PSATicketInfo` Pydantic schema to pass them through.
`AssistantChatPage` already stores the linked ticket as `linkedTicket: PSATicketInfo | null` in local state (populated when a ticket is linked via `TicketLinkIndicator`). Once `PSATicketInfo` includes the IDs, the modal receives:
```typescript
initialValues: {
company_id: linkedTicket.company_id, // from session's linked ticket
company_id: linkedTicket.company_id,
board_id: linkedTicket.board_id,
}
```
@@ -348,11 +388,13 @@ No new backend endpoints — the modal reuses `POST /integrations/psa/tickets` a
### Placement
In the collapsible Dashboard section of `QuickStartPage`, alongside `PendingEscalations` and `ActiveFlowPilotSessions`. Component: `MyQueueWidget`.
`TicketQueue` **already exists** in `QuickStartPage` (line 64, below `ActiveFlowPilotSessions`, above the Dashboard section). It currently auto-hides if no PSA connection exists. This spec updates the existing `TicketQueue` component — it is **not** a new widget and does not need to be added to `QuickStartPage`. The Dashboard section below it is not collapsible.
### Data Fetching
On mount: `GET /integrations/psa/member-mappings` first to detect mapping state, then `searchTickets({ assigned_to_me: true, include_closed: false, page_size: 5 })` if a mapping exists for the current user.
On mount: `GET /integrations/psa/member-mappings` first to detect mapping state, then `integrationsApi.searchTicketsQueue({ assigned_to_me: true, include_closed: false, page_size: 5 })` if a mapping exists for the current user.
`searchTicketsQueue` is used (not `searchTickets`) because it already accepts `assigned_to_me` and `page_size` params. Its return type will be updated to `TicketListResponse` as part of the search endpoint migration, so the widget reads `.items` after that change.
Member mapping detection is explicit — the widget checks the mappings response, not the ticket result. "No mapping" and "no tickets" are distinct states.
@@ -391,8 +433,10 @@ Backend `search_tickets()` adds `orderBy=priority desc,dateEntered desc` to the
### Modified Backend Files
- `backend/app/api/endpoints/integrations.py` — 6 new endpoints, update search to return `TicketListResponse`
- `backend/app/services/psa/base.py`4 new abstract methods
- `backend/app/services/psa/connectwise/provider.py` — implement 4 new methods
- `backend/app/services/psa/types.py`add `PaginatedTicketResult` dataclass
- `backend/app/services/psa/base.py` — 4 new abstract methods; update `search_tickets` return type to `PaginatedTicketResult`
- `backend/app/services/psa/connectwise/provider.py` — implement 4 new methods; update `search_tickets` to fire parallel count request and return `PaginatedTicketResult`; update `_map_ticket()` to pass through `company_id` and `board_id`
- `backend/app/schemas/psa_connection.py` — add `company_id` and `board_id` to `PSATicketInfo` Pydantic schema
- `backend/app/services/assistant_chat_service.py` — add spin-off ticket rule to `ASSISTANT_SYSTEM_PROMPT`
- ~~`backend/app/services/flowpilot_engine.py`~~ — no changes (FlowPilot out of scope for this feature)
- ~~`backend/app/services/unified_chat_service.py`~~ — no changes (existing `[ACTIONS]` parser handles the format)
@@ -419,7 +463,8 @@ Backend `search_tickets()` adds `orderBy=priority desc,dateEntered desc` to the
- `frontend/src/pages/AssistantChatPage.tsx` — handle `create_spin_off_ticket` command in action renderer + add "New Ticket" button to session header
- `frontend/src/pages/QuickStartPage.tsx` — MyQueueWidget
- `frontend/src/api/integrations.ts` — update `searchTickets()` and `searchTicketsQueue()` return types to `TicketListResponse`
- `frontend/src/components/dashboard/TicketQueue.tsx`read `.items` from paginated response
- `frontend/src/types/integrations.ts` — add `company_id: number | null` and `board_id: number | null` to `PSATicketInfo`
- `frontend/src/components/dashboard/TicketQueue.tsx` — update existing component: read `.items`, add mapping-state detection, member-mapping check, and 5-item cap
- `frontend/src/components/session/TicketPickerModal.tsx` — read `.items` from paginated response
---