feat(psa): ticket queue dashboard with board selector and session auto-start
- 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:
@@ -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")
|
||||
|
||||
|
||||
@@ -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]:
|
||||
...
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user