feat(psa): add ticket picker modal and session header ticket link indicator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-14 22:56:21 -04:00
parent 7eaab77daa
commit b76864a892
8 changed files with 393 additions and 1 deletions

View File

@@ -22,6 +22,10 @@ import { buildSessionShareUrl, getLatestActiveShareForSession } from '@/lib/sess
import { CopilotPanel } from '@/components/copilot/CopilotPanel'
import { CopilotToggle } from '@/components/copilot/CopilotToggle'
import { Button } from '@/components/ui/Button'
import { integrationsApi, sessionPsaApi } from '@/api/integrations'
import { TicketPickerModal } from '@/components/session/TicketPickerModal'
import { TicketLinkIndicator } from '@/components/session/TicketLinkIndicator'
import type { PSATicketInfo } from '@/types/integrations'
interface LocationState {
sessionId?: string
@@ -65,6 +69,11 @@ export function TreeNavigationPage() {
const sharePopoverRef = useRef<HTMLDivElement>(null)
const [copilotOpen, setCopilotOpen] = useState(false)
// PSA ticket link state
const [hasConnection, setHasConnection] = useState(false)
const [showTicketPicker, setShowTicketPicker] = useState(false)
const [ticketInfo, setTicketInfo] = useState<PSATicketInfo | null>(null)
const handleCopyCommand = (text: string) => {
navigator.clipboard.writeText(text)
setCopiedCommand(text)
@@ -272,6 +281,32 @@ export function TreeNavigationPage() {
}
}, [treeId])
// Check for PSA connection on mount
useEffect(() => {
integrationsApi.getConnection()
.then((conn) => setHasConnection(!!conn))
.catch(() => setHasConnection(false))
}, [])
const handleTicketLinked = (linkedTicketId: string, ticket: PSATicketInfo) => {
setTicketInfo(ticket)
setSession((prev) => prev ? { ...prev, psa_ticket_id: linkedTicketId } : prev)
setShowTicketPicker(false)
toast.success(`Linked to CW #${linkedTicketId}`)
}
const handleTicketUnlink = async () => {
if (!session) return
try {
await sessionPsaApi.linkTicket(session.id, null)
setSession((prev) => prev ? { ...prev, psa_ticket_id: null } : prev)
setTicketInfo(null)
toast.success('Ticket unlinked')
} catch {
toast.error('Failed to unlink ticket')
}
}
const loadTreeAndSession = async () => {
setIsLoading(true)
setError(null)
@@ -656,6 +691,15 @@ export function TreeNavigationPage() {
{clientName && `Client: ${clientName}`}
</p>
)}
{session && (
<TicketLinkIndicator
session={session}
hasConnection={hasConnection}
onLinkClick={() => setShowTicketPicker(true)}
onUnlink={handleTicketUnlink}
ticketInfo={ticketInfo}
/>
)}
</div>
<div className="flex items-center gap-2">
{/* Share Progress Popover */}
@@ -1251,6 +1295,16 @@ export function TreeNavigationPage() {
onClose={() => setShowShareModal(false)}
/>
)}
{/* Ticket Picker Modal */}
{session && (
<TicketPickerModal
open={showTicketPicker}
onClose={() => setShowTicketPicker(false)}
sessionId={session.id}
onLinked={handleTicketLinked}
/>
)}
</div>
</div>