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

@@ -24,6 +24,10 @@ import type { CustomStepDraft } from '@/components/step-library/CustomStepModal'
import { PostStepActionModal } from '@/components/session/PostStepActionModal'
import { CopilotPanel } from '@/components/copilot/CopilotPanel'
import { CopilotToggle } from '@/components/copilot/CopilotToggle'
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 StepState {
notes: string
@@ -86,6 +90,11 @@ export function ProceduralNavigationPage() {
const [isSavingStep, setIsSavingStep] = useState(false)
const [copilotOpen, setCopilotOpen] = useState(false)
// PSA ticket link state
const [hasConnection, setHasConnection] = useState(false)
const [showTicketPicker, setShowTicketPicker] = useState(false)
const [psaTicketInfo, setPsaTicketInfo] = useState<PSATicketInfo | null>(null)
// Editable variables panel state
const [editingVarName, setEditingVarName] = useState<string | null>(null)
const [editingVarValue, setEditingVarValue] = useState('')
@@ -131,6 +140,32 @@ export function ProceduralNavigationPage() {
}
}, [treeId])
// Check for PSA connection on mount
useEffect(() => {
integrationsApi.getConnection()
.then((conn) => setHasConnection(!!conn))
.catch(() => setHasConnection(false))
}, [])
const handleTicketLinked = (linkedTicketId: string, ticket: PSATicketInfo) => {
setPsaTicketInfo(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)
setPsaTicketInfo(null)
toast.success('Ticket unlinked')
} catch {
toast.error('Failed to unlink ticket')
}
}
// Parse backend timestamp — ensure UTC if no timezone info
const parseTimestamp = (ts: string) => {
if (!ts.endsWith('Z') && !ts.includes('+') && !/\d{2}:\d{2}$/.test(ts.slice(-5))) {
@@ -584,6 +619,17 @@ export function ProceduralNavigationPage() {
Exit
</button>
</div>
{session && (
<div className="mt-1.5">
<TicketLinkIndicator
session={session}
hasConnection={hasConnection}
onLinkClick={() => setShowTicketPicker(true)}
onUnlink={handleTicketUnlink}
ticketInfo={psaTicketInfo}
/>
</div>
)}
<div className="mt-2">
<ProgressBar
currentStep={completedStepIds.size}
@@ -877,6 +923,16 @@ export function ProceduralNavigationPage() {
{treeId && (
<CopilotToggle isOpen={copilotOpen} onToggle={() => setCopilotOpen(true)} />
)}
{/* Ticket Picker Modal */}
{session && (
<TicketPickerModal
open={showTicketPicker}
onClose={() => setShowTicketPicker(false)}
sessionId={session.id}
onLinked={handleTicketLinked}
/>
)}
</div>
)
}