feat(tickets): add tickets API client, update integrations API for paginated search, fix callers

- Create frontend/src/api/tickets.ts with ticketsApi (resources, status, create, ai-parse, priorities, search)
- Update integrationsApi.searchTickets and searchTicketsQueue return types from PSATicketSearchResult[] to TicketListResponse
- Fix TicketQueue.tsx to use results.items (append/set) and results.items.length for pagination check
- Fix TicketPickerModal.tsx to use results.items when setting search results
- Export ticketsApi from api/index.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 03:05:13 +00:00
parent 506aac609d
commit 9d88c8456c
5 changed files with 58 additions and 8 deletions

View File

@@ -37,3 +37,4 @@ export { handoffsApi } from './handoffs'
export { resolutionsApi } from './resolutions'
export { deviceTypesApi } from './deviceTypes'
export { networkDiagramsApi } from './networkDiagrams'
export { ticketsApi } from './tickets'

View File

@@ -1,6 +1,7 @@
import { apiClient } from './client'
import type { PsaConnectionResponse, PsaConnectionCreate, PsaConnectionUpdate, PsaConnectionTestResponse } from '@/types'
import type { PSABoard, TicketLinkResponse, PSATicketSearchResult, PSATicketInfo, PSATicketStatusItem, PsaPreviewResponse, PsaPostResponse, PsaPostLogEntry, PsaMemberResponse, PsaMemberMappingResponse, AutoMatchResult, FlowpilotSettings } from '@/types/integrations'
import type { TicketListResponse } from '@/types/tickets'
export const integrationsApi = {
getConnection: () =>
@@ -15,16 +16,16 @@ export const integrationsApi = {
apiClient.post<PsaConnectionTestResponse>(`/integrations/psa/connections/${id}/test`).then(r => r.data),
listBoards: () =>
apiClient.get<PSABoard[]>('/integrations/psa/boards').then(r => r.data),
searchTickets: (params: { query?: string; board_id?: number; include_closed?: boolean }) =>
apiClient.get<PSATicketSearchResult[]>('/integrations/psa/tickets/search', { params }).then(r => r.data),
searchTickets: (params: { query?: string; board_id?: number; include_closed?: boolean }): Promise<TicketListResponse> =>
apiClient.get<TicketListResponse>('/integrations/psa/tickets/search', { params }).then(r => r.data),
searchTicketsQueue: (params: {
assigned_to_me?: boolean
unassigned?: boolean
board_ids?: string
page?: number
page_size?: number
}) =>
apiClient.get<PSATicketSearchResult[]>('/integrations/psa/tickets/search', { params }).then(r => r.data),
}): Promise<TicketListResponse> =>
apiClient.get<TicketListResponse>('/integrations/psa/tickets/search', { params }).then(r => r.data),
getTicket: (id: string) =>
apiClient.get<PSATicketInfo>(`/integrations/psa/tickets/${id}`).then(r => r.data),
getTicketStatuses: (ticketId: string) =>

View File

@@ -0,0 +1,48 @@
import { apiClient } from './client'
import type {
PSAResource,
PSATicketCreated,
PSATicketStatusUpdate,
TicketCreationPayload,
AiParseResponse,
TicketListResponse,
PSAPriority,
} from '@/types/tickets'
export const ticketsApi = {
listResources: (ticketId: number): Promise<PSAResource[]> =>
apiClient.get<PSAResource[]>(`/integrations/psa/tickets/${ticketId}/resources`).then(r => r.data),
addResource: (ticketId: number, memberId: number): Promise<PSAResource> =>
apiClient.post<PSAResource>(`/integrations/psa/tickets/${ticketId}/resources?member_id=${memberId}`).then(r => r.data),
removeResource: (ticketId: number, memberId: number): Promise<void> =>
apiClient.delete(`/integrations/psa/tickets/${ticketId}/resources/${memberId}`).then(() => undefined),
updateStatus: (ticketId: number, statusId: number): Promise<PSATicketStatusUpdate> =>
apiClient.patch<PSATicketStatusUpdate>(`/integrations/psa/tickets/${ticketId}/status?status_id=${statusId}`).then(r => r.data),
createTicket: (payload: TicketCreationPayload): Promise<PSATicketCreated> =>
apiClient.post<PSATicketCreated>('/integrations/psa/tickets', payload).then(r => r.data),
aiParse: (prompt: string): Promise<AiParseResponse> =>
apiClient.post<AiParseResponse>('/integrations/psa/tickets/ai-parse', { prompt }).then(r => r.data),
listPriorities: (): Promise<PSAPriority[]> =>
apiClient.get<PSAPriority[]>('/integrations/psa/priorities').then(r => r.data),
searchTickets: (params: {
query?: string
board_id?: number | null
status_id?: number | null
include_closed?: boolean
assigned_to_me?: boolean
unassigned?: boolean
board_ids?: string
priority?: string | null
company_id?: number | null
page?: number
page_size?: number
}): Promise<TicketListResponse> =>
apiClient.get<TicketListResponse>('/integrations/psa/tickets/search', { params }).then(r => r.data),
}

View File

@@ -234,11 +234,11 @@ export function TicketQueue() {
try {
const results = await integrationsApi.searchTicketsQueue(params)
if (append) {
setTickets((prev) => [...prev, ...results])
setTickets((prev) => [...prev, ...results.items])
} else {
setTickets(results)
setTickets(results.items)
}
setHasMore(results.length === PAGE_SIZE)
setHasMore(results.items.length === PAGE_SIZE)
setError(null)
} catch {
setError('Failed to load tickets. Check your PSA connection.')

View File

@@ -56,7 +56,7 @@ export function TicketPickerModal({ open, onClose, sessionId, onLinked, onSelect
query: query.trim(),
include_closed: closed,
})
setSearchResults(results)
setSearchResults(results.items)
setHasSearched(true)
} catch (err: unknown) {
const message =