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:
@@ -27,6 +27,7 @@ from app.schemas.psa_connection import (
|
||||
PsaMemberMappingSaveRequest,
|
||||
PsaMemberResponse,
|
||||
AutoMatchResult,
|
||||
PSABoardResponse,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.services.psa.encryption import (
|
||||
@@ -345,26 +346,91 @@ async def update_flowpilot_settings(
|
||||
# ── ticket / status / company endpoints ──────────────────────────
|
||||
|
||||
|
||||
@router.get("/tickets/search", response_model=list[PSATicketSearchResult])
|
||||
async def search_tickets(
|
||||
@router.get("/boards", response_model=list[PSABoardResponse])
|
||||
async def list_boards(
|
||||
current_user: Annotated[User, Depends(require_engineer_or_admin)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
query: str = "",
|
||||
board_id: int | None = None,
|
||||
status_id: int | None = None,
|
||||
include_closed: bool = False,
|
||||
):
|
||||
"""Search ConnectWise tickets."""
|
||||
"""List PSA service boards."""
|
||||
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)
|
||||
boards = await provider.list_boards()
|
||||
return [PSABoardResponse(id=b.id, name=b.name) for b in boards]
|
||||
except PSAError as e:
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/tickets/search", response_model=list[PSATicketSearchResult])
|
||||
async def search_tickets(
|
||||
current_user: Annotated[User, Depends(require_engineer_or_admin)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
query: str = "",
|
||||
board_id: int | None = None,
|
||||
status_id: int | None = None,
|
||||
include_closed: bool = False,
|
||||
assigned_to_me: bool = False,
|
||||
unassigned: bool = False,
|
||||
board_ids: str = "",
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
):
|
||||
"""Search ConnectWise tickets."""
|
||||
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
|
||||
|
||||
# Resolve assigned_to_me → member_id
|
||||
member_id: str | None = None
|
||||
if assigned_to_me:
|
||||
conn_result = await db.execute(
|
||||
select(PsaConnection).where(
|
||||
PsaConnection.account_id == current_user.account_id,
|
||||
PsaConnection.is_active.is_(True),
|
||||
)
|
||||
)
|
||||
conn = conn_result.scalar_one_or_none()
|
||||
if conn:
|
||||
mapping_result = await db.execute(
|
||||
select(PsaMemberMapping).where(
|
||||
PsaMemberMapping.psa_connection_id == conn.id,
|
||||
PsaMemberMapping.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
mapping = mapping_result.scalar_one_or_none()
|
||||
if mapping:
|
||||
member_id = mapping.external_member_id
|
||||
else:
|
||||
# No mapping for this user — return empty list
|
||||
return []
|
||||
|
||||
# Parse comma-separated board_ids
|
||||
parsed_board_ids: list[int] = []
|
||||
if board_ids:
|
||||
try:
|
||||
parsed_board_ids = [int(bid.strip()) for bid in board_ids.split(",") if bid.strip()]
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="board_ids must be comma-separated integers")
|
||||
|
||||
try:
|
||||
provider = await get_provider_for_account(current_user.account_id, db)
|
||||
tickets = await provider.search_tickets(
|
||||
query, board_id=board_id, status_id=status_id, include_closed=include_closed
|
||||
query,
|
||||
board_id=board_id,
|
||||
status_id=status_id,
|
||||
include_closed=include_closed,
|
||||
member_id=member_id,
|
||||
unassigned=unassigned,
|
||||
board_ids=parsed_board_ids,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return [
|
||||
PSATicketSearchResult(
|
||||
|
||||
Reference in New Issue
Block a user