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
Owner

Summary

PSA Ticket Management — full ticket management workflow integrated with ConnectWise.

Core features:

  • /tickets page with URL-param filter state (board, status, priority, company, assigned, closed) and paginated list
  • TicketDetailPanel — notes feed, configurations, related tickets, resource manager, optimistic status updates
  • NewTicketModal — AI Quick Create tab (natural language → pre-filled form via Claude) + Full Form tab
  • Spin-off ticket creation from ResolutionAssist — AI surfaces the modal pre-filled with session context
  • TicketQueue dashboard widget updated: member mapping detection, 5-item cap, View All link

Backend:

  • ticket_service.py service layer for all PSA mutations
  • Paginated search_tickets with parallel CW count fetch
  • New endpoints: create ticket, update status, list/add/remove resources, priorities, board statuses
  • AI parse endpoint uses Claude to extract structured ticket data from natural language

Fixes (pre-landing review):

  • CW condition injection: sanitize search query (strip single quotes)
  • company_id filter now applied to CW query conditions (was silently ignored)
  • PSA error surfacing: inline error banner on /tickets when CW returns 403/permissions error
  • linkedTicket race condition guard in ResolutionAssist
  • Members auth gate, board fallback from ticket data

CW Security Role Requirements

The API member needs these permissions in their security role:

Module Action Verb Level
Service Desk Service Tickets Inquire ALL
Service Desk Close Service Tickets Inquire ALL
System Table Setup Inquire ALL
System Member Maintenance Inquire ALL

Reference: docs/connectwise/CW_Security_Roles/api-member-security-roles.yaml

Pre-Landing Review

2 issues found and fixed:

  • [P2] (confidence: 9/10) CW condition injection via unescaped query string
  • [P2] (confidence: 9/10) company_id filter silently ignored in CW provider

Test plan

  • TypeScript build clean (npx tsc -b — verified)
  • Backend tests: pytest tests/test_psa_tickets.py (requires VPS SSH — Python not available in code-server)
  • Visit /tickets — verify ticket list loads or shows CW permissions error banner
  • Select a board — verify status dropdown populates
  • Open a ticket — verify detail panel loads with notes and resources
  • New Ticket → Quick Create tab — enter natural language, verify AI parse pre-fills form
  • Spin-off ticket from ResolutionAssist session

🤖 Generated with Claude Code

## Summary **PSA Ticket Management** — full ticket management workflow integrated with ConnectWise. **Core features:** - `/tickets` page with URL-param filter state (board, status, priority, company, assigned, closed) and paginated list - `TicketDetailPanel` — notes feed, configurations, related tickets, resource manager, optimistic status updates - `NewTicketModal` — AI Quick Create tab (natural language → pre-filled form via Claude) + Full Form tab - Spin-off ticket creation from ResolutionAssist — AI surfaces the modal pre-filled with session context - `TicketQueue` dashboard widget updated: member mapping detection, 5-item cap, View All link **Backend:** - `ticket_service.py` service layer for all PSA mutations - Paginated `search_tickets` with parallel CW count fetch - New endpoints: create ticket, update status, list/add/remove resources, priorities, board statuses - AI parse endpoint uses Claude to extract structured ticket data from natural language **Fixes (pre-landing review):** - CW condition injection: sanitize search query (strip single quotes) - `company_id` filter now applied to CW query conditions (was silently ignored) - PSA error surfacing: inline error banner on `/tickets` when CW returns 403/permissions error - `linkedTicket` race condition guard in ResolutionAssist - Members auth gate, board fallback from ticket data ## CW Security Role Requirements The API member needs these permissions in their security role: | Module | Action | Verb | Level | |--------|--------|------|-------| | Service Desk | Service Tickets | Inquire | ALL | | Service Desk | Close Service Tickets | Inquire | ALL | | System | Table Setup | Inquire | ALL | | System | Member Maintenance | Inquire | ALL | Reference: `docs/connectwise/CW_Security_Roles/api-member-security-roles.yaml` ## Pre-Landing Review 2 issues found and fixed: - `[P2]` (confidence: 9/10) CW condition injection via unescaped query string - `[P2]` (confidence: 9/10) `company_id` filter silently ignored in CW provider ## Test plan - [ ] TypeScript build clean (`npx tsc -b` — verified) - [ ] Backend tests: `pytest tests/test_psa_tickets.py` (requires VPS SSH — Python not available in code-server) - [ ] Visit `/tickets` — verify ticket list loads or shows CW permissions error banner - [ ] Select a board — verify status dropdown populates - [ ] Open a ticket — verify detail panel loads with notes and resources - [ ] New Ticket → Quick Create tab — enter natural language, verify AI parse pre-fills form - [ ] Spin-off ticket from ResolutionAssist session 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chihlasm added 32 commits 2026-04-16 14:44:42 +00:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Explicitly call out search_tickets breaking change and all existing callers
- Fix [ACTIONS] marker to use JSON array format matching existing parser
- Route system prompt change to assistant_chat_service.py, not flowpilot_engine
- Pivot detail panel hydration to existing getTicketContext + listResources

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Define PaginatedTicketResult provider type + parallel count fetch via CW /count endpoint
- Fix dashboard widget: updates existing TicketQueue (not new), uses searchTicketsQueue
- Fix NewTicketModal prefill: expand PSATicketInfo with company_id/board_id fields
- Correct Dashboard section description: not collapsible, TicketQueue already exists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace false claim about linkedTicket state with explicit fetch step on modal open
- Remove MyQueueWidget references; TicketQueue is the existing component being updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Tickets page route to router with lazy loading and code splitting.
Add Tickets navigation entry to sidebar in RESOLVE section for both
icon rail and pinned layouts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TicketDetailHeader: Display ticket info with status dropdown
- TicketNotesFeed: Chronological list of ticket notes with internal flag
- TicketAddNote: Form to add notes (requires linked session)
- TicketConfigs: Display related configurations/devices
- TicketRelated: List of related tickets as clickable buttons

All components use type-safe imports from psaContext and integrations APIs.
Styling follows design system (flat dark theme, electric blue accent, Tailwind v4).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat(tickets): update TicketQueue with mapping detection, 5-item cap, View All link
All checks were successful
Mirror to GitHub / mirror (push) Successful in 4s
00cd8b7c55
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(tickets): fix permissions toast, board fallback, assignment search, remove load more
All checks were successful
Mirror to GitHub / mirror (push) Successful in 2s
6044d5a88b
- list_resources: return [] on PSAError instead of 502 — stops global interceptor
  toast when CW API key lacks ticket members permission (Lesson 111)
- list_boards/list_priorities: add warning logging so Railway logs reveal the
  root cause when CW permissions are missing
- TicketsPage: derive board options from ticket search results when listBoards
  returns empty (CW permissions fallback)
- TicketFilterBar: replace assignment <select> with searchable member picker —
  fixed options (All/Mine/Unassigned) + text-filtered member dropdown
- TicketQueue: remove Load More / infinite scroll; page now exists at /tickets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(tickets): fix statuses endpoint, members auth gate, and graceful error handling
All checks were successful
Mirror to GitHub / mirror (push) Successful in 3s
fb7690485b
- Add GET /boards/{board_id}/statuses endpoint — direct board-to-statuses lookup
  without ticket roundabout; used by filter bar and new ticket form
- Fix TicketsPage and NewTicketModal to call getBoardStatuses(board_id) instead
  of misusing getTicketStatuses(ticket_id) with a board_id value
- Fix list_members auth: was require_account_owner (owner/super_admin only) —
  changed to require_engineer_or_admin so engineers can see member list for
  ticket assignment
- list_members: return [] on PSAError instead of 502 (Lesson 111 pattern)
- get_ticket_statuses: return [] on PSAError instead of 502

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Apply company_id filter in CW search_tickets conditions (was silently ignored)
- Sanitize query string to strip single quotes before CW condition interpolation
- Add psaError state to TicketsPage for permissions error surfacing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chore: bump version and changelog (v0.1.0.0)
Some checks failed
Mirror to GitHub / mirror (push) Successful in 4s
CI / backend (pull_request) Failing after 18m54s
CI / frontend (pull_request) Failing after 47s
CI / e2e (pull_request) Has been skipped
bea34229d6
Add CW security roles reference docs and PSA ticket management plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chihlasm added 1 commit 2026-04-16 21:03:02 +00:00
fix(tickets): status filter dropdown and CW resource assignment
Some checks failed
Mirror to GitHub / mirror (push) Successful in 4s
CI / backend (pull_request) Failing after 17m51s
CI / frontend (pull_request) Failing after 52s
CI / e2e (pull_request) Has been skipped
60851b400a
- Status filter: aggregate statuses across all boards (deduped by name)
  when no board is selected. Backend accepts status_name and filters by
  status/name so the same status matches across boards.
- Resource assignment: CW has no /service/tickets/{id}/members endpoint —
  assignees live in the ticket's comma-separated `resources` string field.
  Rewrote list/add/remove to read/PATCH that field via member identifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chihlasm added 1 commit 2026-04-16 21:28:50 +00:00
fix(tickets): refresh status and resources in detail panel after update
Some checks failed
Mirror to GitHub / mirror (push) Successful in 3s
CI / backend (pull_request) Failing after 17m32s
CI / frontend (pull_request) Failing after 48s
CI / e2e (pull_request) Has been skipped
04ff2ea301
Status update was returning only new_status (string) and the parent list's
onStatusUpdated only set status_name. The <select> was bound to status_id,
which never changed — so it visually reverted to the old status even though
the PATCH succeeded.

- Backend: include new_status_id in the status-update response.
- Panel: own currentStatusId/currentStatusName state so the select reflects
  the change immediately and survives stale parent snapshots.
- Parent list: update status_id on both the row and selectedTicket so the
  list row stays in sync when the panel stays open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chihlasm added 1 commit 2026-04-16 21:39:23 +00:00
fix(psa): resource assignment targets CW owner, status PATCH verifies apply
Some checks failed
Mirror to GitHub / mirror (push) Successful in 2s
CI / backend (pull_request) Failing after 15m32s
CI / frontend (pull_request) Failing after 45s
CI / e2e (pull_request) Has been skipped
f6a24ea4e1
Previous `resources`-string PATCH was silently ignored by CW — the
`resources` field is server-derived from the ticket's owner + schedule
entries, not freely writable. Status PATCH could also silently no-op
when a cross-board status id was sent.

- add_resource: when the ticket is unassigned, set the `owner`
  MemberReference (the canonical writable primary-assignee field).
  If already owned by someone else, append the identifier to the
  `resources` co-assignee string best-effort.
- remove_resource: clear `owner` (with remove→replace:null fallback) if
  the target is the current owner, otherwise strip from `resources`.
- list_resources: merge owner + resources string, deduped by member id,
  so the UI reflects both single-owner and multi-resource assignments.
- update_ticket_status: verify CW applied the status by comparing the
  response body's status.id — raises PSAError with a clear message when
  CW silently rejects the change (e.g., status invalid for ticket's
  board), instead of reporting spurious success.
- Frontend: surface the backend error detail in the toast so users see
  the real reason instead of a generic "Failed to update" message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chihlasm added 1 commit 2026-04-17 00:34:21 +00:00
fix(psa): use schedule entries for ticket co-assignees (CW canonical pattern)
Some checks failed
Mirror to GitHub / mirror (push) Successful in 33s
CI / backend (pull_request) Failing after 17m0s
CI / frontend (pull_request) Failing after 51s
CI / e2e (pull_request) Has been skipped
995a0c1d2e
The previous implementation PATCHed the `resources` string directly, which CW
silently ignores because `resources` is a server-derived read-only field (it's
populated from schedule entries of type/id=4, not freely writable).

Per CW docs (openapi line 70949): "Please use the
/schedule/entries?conditions=type/id=4 AND objectId={id} endpoint".

Behavior per spec:
- No owner + assign user → set owner (existing behavior kept)
- Has owner + assign different user → POST /schedule/entries with type/id=4,
  member, objectId; owner untouched
- User already assigned (owner or schedule entry) → idempotent no-op
- Remove owner → clear owner (existing behavior kept)
- Remove co-assignee → DELETE /schedule/entries/{entry_id}
- list_resources now merges owner + schedule-entry members, deduped by id

Required CW security role permission on the API member:
- Service > Resource Scheduling > Add/Inquire/Delete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chihlasm merged commit 16060d2235 into main 2026-04-25 04:59:02 +00:00
Sign in to join this conversation.