feat: PSA ticket management — /tickets page, detail panel, AI ticket creation #141

Merged
chihlasm merged 36 commits from feat/psa-ticket-management into main 2026-04-25 04:59:02 +00:00
4 changed files with 32 additions and 5 deletions
Showing only changes of commit fb7690485b - Show all commits

View File

@@ -788,7 +788,30 @@ async def get_ticket_statuses(
except PSANotFoundError:
raise HTTPException(status_code=404, detail="Ticket not found")
except PSAError as e:
raise HTTPException(status_code=502, detail=str(e))
logger.warning("get_ticket_statuses(%s) failed: %s", ticket_id, e)
return []
@router.get("/boards/{board_id}/statuses", response_model=list[PSATicketStatusItem])
async def get_board_statuses(
board_id: int,
current_user: Annotated[User, Depends(require_engineer_or_admin)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""Get available statuses for a service board directly (no ticket lookup required)."""
if not current_user.account_id:
raise HTTPException(status_code=400, detail="User has no account")
from app.services.psa.registry import get_provider_for_account
from app.services.psa.exceptions import PSAError
try:
provider = await get_provider_for_account(current_user.account_id, db)
statuses = await provider.get_ticket_statuses(board_id)
return [PSATicketStatusItem(id=s.id, name=s.name, is_closed=s.is_closed) for s in statuses]
except PSAError as e:
logger.warning("get_board_statuses(%s) failed: %s", board_id, e)
return []
# ── member mapping endpoints ─────────────────────────────────────────
@@ -796,7 +819,7 @@ async def get_ticket_statuses(
@router.get("/members", response_model=list[PsaMemberResponse])
async def list_members(
current_user: Annotated[User, Depends(require_account_owner)],
current_user: Annotated[User, Depends(require_engineer_or_admin)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""List CW members (from CW API)."""
@@ -814,7 +837,9 @@ async def list_members(
for m in members
]
except PSAError as e:
raise HTTPException(status_code=502, detail=str(e))
# Members are optional display data — degrade gracefully
logger.warning("list_members failed: %s", e)
return []
@router.get("/member-mappings", response_model=list[PsaMemberMappingResponse])

View File

@@ -30,6 +30,8 @@ export const integrationsApi = {
apiClient.get<PSATicketInfo>(`/integrations/psa/tickets/${id}`).then(r => r.data),
getTicketStatuses: (ticketId: string) =>
apiClient.get<PSATicketStatusItem[]>(`/integrations/psa/tickets/${ticketId}/statuses`).then(r => r.data),
getBoardStatuses: (boardId: number | string) =>
apiClient.get<PSATicketStatusItem[]>(`/integrations/psa/boards/${boardId}/statuses`).then(r => r.data),
listMembers: () =>
apiClient.get<PsaMemberResponse[]>('/integrations/psa/members').then(r => r.data),
getMemberMappings: () =>

View File

@@ -44,7 +44,7 @@ export function NewTicketModal({ defaultTab = 'quick', initialValues, summaryHin
useEffect(() => {
if (draft.board_id) {
integrationsApi.getTicketStatuses(String(draft.board_id))
integrationsApi.getBoardStatuses(draft.board_id)
.then(setStatuses).catch(() => {})
} else {
setStatuses([])

View File

@@ -56,7 +56,7 @@ export default function TicketsPage() {
// Load statuses when board changes
useEffect(() => {
if (filters.board_id) {
integrationsApi.getTicketStatuses(String(filters.board_id))
integrationsApi.getBoardStatuses(filters.board_id)
.then(setStatuses).catch(() => {})
} else {
setStatuses([])