feat(psa): ticket queue dashboard with board selector and session auto-start
Some checks failed
CI / frontend (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / backend (push) Has been cancelled
Mirror to GitHub / mirror (push) Successful in 2s

- Add PSABoard type + list_boards() to CW provider (cached 1h)
- Extend search_tickets with assigned_to_me, unassigned, board_ids, page, page_size
- New GET /integrations/psa/boards endpoint
- New TicketQueue dashboard component: My Tickets / Unassigned tabs,
  multi-select board filter, Load more pagination, Start Session per ticket
- Add TicketQueue to QuickStartPage after active sessions
- FlowPilotSessionPage auto-starts with ticket context when navigated
  from TicketQueue (psaTicketId + psaTicket in location.state)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 03:20:45 +00:00
parent b18072e24b
commit 346576a730
14 changed files with 595 additions and 16 deletions

View File

@@ -11,6 +11,7 @@ from app.services.psa.types import (
PSAMember,
PSAConfiguration,
PSATimeEntry,
PSABoard,
)
@@ -58,6 +59,9 @@ class AutotaskProvider(PSAProvider):
async def list_members(self) -> list[PSAMember]:
raise NotImplementedError("Autotask integration coming soon")
async def list_boards(self) -> list[PSABoard]:
raise NotImplementedError("list_boards not implemented for this provider")
async def get_ticket_configurations(self, ticket_id: str) -> list[PSAConfiguration]:
raise NotImplementedError("Autotask integration coming soon")

View File

@@ -12,6 +12,7 @@ from .types import (
PSAMember,
PSAConfiguration,
PSATimeEntry,
PSABoard,
)
@@ -64,6 +65,10 @@ class PSAProvider(ABC):
async def list_members(self) -> list[PSAMember]:
...
@abstractmethod
async def list_boards(self) -> list[PSABoard]:
...
@abstractmethod
async def get_ticket_configurations(self, ticket_id: str) -> list[PSAConfiguration]:
...

View File

@@ -16,6 +16,7 @@ from app.services.psa.types import (
PSAMember,
PSAConfiguration,
PSATimeEntry,
PSABoard,
)
from .client import ConnectWiseClient
@@ -55,11 +56,16 @@ class ConnectWiseProvider(PSAProvider):
return self._map_ticket(data)
async def search_tickets(self, query: str, **filters) -> list[PSATicket]:
"""Search CW tickets by summary. Supports board_id and status_id filters."""
"""Search CW tickets by summary. Supports board_id, status_id, member_id,
unassigned, board_ids, page, and page_size filters."""
page_size = filters.get("page_size", 10)
page = filters.get("page", 1)
params: dict = {
"fields": "id,summary,company,board,status,priority,closedFlag",
"orderBy": "id desc",
"pageSize": 25,
"pageSize": page_size,
"page": page,
}
# Build CW condition query
@@ -72,6 +78,14 @@ class ConnectWiseProvider(PSAProvider):
conditions.append(f"status/id = {filters['status_id']}")
if not filters.get("include_closed", False):
conditions.append("closedFlag = false")
if filters.get("member_id") is not None:
conditions.append(f"resources/member/id = {filters['member_id']}")
if filters.get("unassigned", False):
conditions.append("resources = null")
board_ids: list[int] = filters.get("board_ids") or []
if board_ids:
board_cond = " or ".join(f"board/id = {bid}" for bid in board_ids)
conditions.append(f"({board_cond})")
if conditions:
params["conditions"] = " and ".join(conditions)
@@ -270,6 +284,32 @@ class ConnectWiseProvider(PSAProvider):
psa_cache.set(cache_key, result, ttl_seconds=900)
return result
async def list_boards(self) -> list[PSABoard]:
"""List active CW service boards (cached 1 hour)."""
cache_key = "boards"
cached = psa_cache.get(cache_key)
if cached is not None:
return cached
data = await self.client.get(
"/service/boards",
params={
"fields": "id,name,inactiveFlag",
"conditions": "inactiveFlag = false",
"pageSize": 100,
},
)
result = [
PSABoard(
id=b["id"],
name=b["name"],
inactive=b.get("inactiveFlag", False),
)
for b in (data if isinstance(data, list) else [])
]
psa_cache.set(cache_key, result, ttl_seconds=3600)
return result
# ── Ticket Context ────────────────────────────────────────────────
async def get_ticket_context(

View File

@@ -11,6 +11,7 @@ from app.services.psa.types import (
PSAMember,
PSAConfiguration,
PSATimeEntry,
PSABoard,
)
@@ -58,6 +59,9 @@ class HaloPSAProvider(PSAProvider):
async def list_members(self) -> list[PSAMember]:
raise NotImplementedError("Halo PSA integration coming soon")
async def list_boards(self) -> list[PSABoard]:
raise NotImplementedError("list_boards not implemented for this provider")
async def get_ticket_configurations(self, ticket_id: str) -> list[PSAConfiguration]:
raise NotImplementedError("Halo PSA integration coming soon")

View File

@@ -67,6 +67,12 @@ class PSATimeEntry(BaseModel):
created_at: str | None = None
class PSABoard(BaseModel):
id: int
name: str
inactive: bool = False
class NoteType:
INTERNAL_ANALYSIS = "internal_analysis"
RESOLUTION = "resolution"