import { useState, useEffect, useRef, useCallback } from 'react' import { Ticket, Search, AlertCircle, CheckCircle2, Hash, Loader2 } from 'lucide-react' import { Modal } from '@/components/common/Modal' import { Input } from '@/components/ui/Input' import { Button } from '@/components/ui/Button' import { cn } from '@/lib/utils' import { integrationsApi, sessionPsaApi } from '@/api/integrations' import type { PSATicketInfo, PSATicketSearchResult } from '@/types/integrations' type Mode = 'search' | 'manual' interface Props { open: boolean onClose: () => void /** Legacy session linking mode — pass sessionId + onLinked */ sessionId?: string onLinked?: (ticketId: string, ticket: PSATicketInfo) => void /** Selection-only mode — pass onSelect instead. Returns selected ticket without linking. */ onSelect?: (ticketId: string, ticket: PSATicketInfo) => void } export function TicketPickerModal({ open, onClose, sessionId, onLinked, onSelect }: Props) { const [mode, setMode] = useState('search') // Search mode state const [searchQuery, setSearchQuery] = useState('') const [includeClosed, setIncludeClosed] = useState(false) const [searchResults, setSearchResults] = useState([]) const [isSearching, setIsSearching] = useState(false) const [hasSearched, setHasSearched] = useState(false) // Manual mode state const [manualId, setManualId] = useState('') // Shared state const [selectedTicket, setSelectedTicket] = useState(null) const [selectedTicketId, setSelectedTicketId] = useState(null) const [isLooking, setIsLooking] = useState(false) const [isLinking, setIsLinking] = useState(false) const [error, setError] = useState(null) const debounceRef = useRef | null>(null) // Debounced search const performSearch = useCallback(async (query: string, closed: boolean) => { if (!query.trim()) { setSearchResults([]) setHasSearched(false) return } setIsSearching(true) setError(null) try { const results = await integrationsApi.searchTickets({ query: query.trim(), include_closed: closed, }) setSearchResults(results) setHasSearched(true) } catch (err: unknown) { const message = err && typeof err === 'object' && 'response' in err ? (err as { response?: { data?: { detail?: string } } }).response?.data?.detail : null setError(message || 'Failed to search tickets.') setSearchResults([]) setHasSearched(true) } finally { setIsSearching(false) } }, []) // Trigger debounced search when query or includeClosed changes useEffect(() => { if (mode !== 'search') return if (debounceRef.current) { clearTimeout(debounceRef.current) } if (!searchQuery.trim()) { setSearchResults([]) setHasSearched(false) return } debounceRef.current = setTimeout(() => { performSearch(searchQuery, includeClosed) }, 300) return () => { if (debounceRef.current) { clearTimeout(debounceRef.current) } } }, [searchQuery, includeClosed, mode, performSearch]) const handleSelectSearchResult = async (result: PSATicketSearchResult) => { setIsLooking(true) setError(null) try { const ticket = await integrationsApi.getTicket(result.id) setSelectedTicket(ticket) setSelectedTicketId(result.id) } catch (err: unknown) { const message = err && typeof err === 'object' && 'response' in err ? (err as { response?: { data?: { detail?: string } } }).response?.data?.detail : null setError(message || 'Failed to load ticket details.') } finally { setIsLooking(false) } } const handleManualLookup = async () => { const trimmed = manualId.trim() if (!trimmed) return setIsLooking(true) setError(null) setSelectedTicket(null) setSelectedTicketId(null) try { const ticket = await integrationsApi.getTicket(trimmed) setSelectedTicket(ticket) setSelectedTicketId(trimmed) } catch (err: unknown) { const message = err && typeof err === 'object' && 'response' in err ? (err as { response?: { data?: { detail?: string } } }).response?.data?.detail : null setError(message || 'Ticket not found. Please check the ticket number and try again.') } finally { setIsLooking(false) } } const handleLink = async () => { if (!selectedTicket || !selectedTicketId) return // Selection-only mode — return ticket data without linking if (onSelect) { onSelect(selectedTicketId, selectedTicket) handleReset() onClose() return } // Legacy session linking mode if (!sessionId) return setIsLinking(true) setError(null) try { await sessionPsaApi.linkTicket(sessionId, selectedTicketId) onLinked?.(selectedTicketId, selectedTicket) handleReset() } catch (err: unknown) { const message = err && typeof err === 'object' && 'response' in err ? (err as { response?: { data?: { detail?: string } } }).response?.data?.detail : null setError(message || 'Failed to link ticket.') } finally { setIsLinking(false) } } const handleReset = () => { setSearchQuery('') setSearchResults([]) setHasSearched(false) setManualId('') setSelectedTicket(null) setSelectedTicketId(null) setError(null) setIsLooking(false) setIsLinking(false) setIsSearching(false) setIncludeClosed(false) } const handleClose = () => { handleReset() setMode('search') onClose() } const handleClearSelection = () => { setSelectedTicket(null) setSelectedTicketId(null) setError(null) } const handleManualKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && manualId.trim() && !isLooking && !selectedTicket) { handleManualLookup() } } const switchMode = (newMode: Mode) => { setMode(newMode) setSelectedTicket(null) setSelectedTicketId(null) setError(null) } return (
{/* Mode tabs */}
{/* Search mode */} {mode === 'search' && !selectedTicket && (
setSearchQuery(e.target.value)} disabled={isLooking} className="w-full" autoFocus />
{/* Include closed toggle */} {/* Search results */} {isSearching && (
)} {!isSearching && hasSearched && searchResults.length === 0 && (
No tickets found
)} {!isSearching && searchResults.length > 0 && (
{searchResults.map((result) => ( ))}
)} {isLooking && (
Loading ticket details...
)}
)} {/* Manual mode */} {mode === 'manual' && !selectedTicket && (
{ setManualId(e.target.value) if (error) setError(null) }} onKeyDown={handleManualKeyDown} disabled={isLooking} className="flex-1" autoFocus />
)} {/* Error */} {error && (

{error}

)} {/* Selected ticket confirmation card */} {selectedTicket && selectedTicketId && (

CW #{selectedTicketId} — {selectedTicket.summary}

{selectedTicket.company_name && ( {selectedTicket.company_name} )} {selectedTicket.board_name && ( <> {selectedTicket.board_name} )} {selectedTicket.status_name && ( <> {selectedTicket.status_name} )} {selectedTicket.priority_name && ( <> {selectedTicket.priority_name} )}
)} {/* Skip link */}
) }