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:
@@ -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
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user