@@ -2,7 +2,8 @@ import SearchResult from './SearchResult';
22import { useSearchResults , useConsolidatedPolling } from '../../hooks/useCaseSearch' ;
33import { SearchResult as SearchResultType } from '../../../../shared/types' ;
44import { useEffect , useMemo , useState , useRef } from 'react' ;
5- import { ClipboardDocumentIcon , CheckIcon } from '@heroicons/react/24/outline' ;
5+ import { ArrowDownTrayIcon , CheckIcon , ClipboardDocumentIcon } from '@heroicons/react/24/outline' ;
6+ import { ZipCaseClient } from '../../services/ZipCaseClient' ;
67
78type DisplayItem = SearchResultType | 'divider' ;
89
@@ -12,9 +13,12 @@ function CaseResultItem({ searchResult }: { searchResult: SearchResultType }) {
1213
1314export default function SearchResultsList ( ) {
1415 const { data, isLoading, isError, error } = useSearchResults ( ) ;
16+
1517 const [ copied , setCopied ] = useState ( false ) ;
1618 const copiedTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
1719
20+ const [ isExporting , setIsExporting ] = useState ( false ) ;
21+
1822 // Extract batches and create a flat display list with dividers
1923 const displayItems = useMemo ( ( ) => {
2024 if ( ! data || ! data . results || ! data . searchBatches ) {
@@ -111,7 +115,7 @@ export default function SearchResultsList() {
111115 } ;
112116 } , [ searchResults , polling ] ) ;
113117
114- // Cleanup timeout on unmount
118+ // Clean up timeout on unmount
115119 useEffect ( ( ) => {
116120 return ( ) => {
117121 if ( copiedTimeoutRef . current ) {
@@ -120,6 +124,38 @@ export default function SearchResultsList() {
120124 } ;
121125 } , [ ] ) ;
122126
127+ const handleExport = async ( ) => {
128+ const caseNumbers = searchResults . map ( r => r . zipCase . caseNumber ) ;
129+ if ( caseNumbers . length === 0 ) return ;
130+
131+ setIsExporting ( true ) ;
132+
133+ // Set a timeout to reset the exporting state after 10 seconds
134+ const timeoutId = setTimeout ( ( ) => {
135+ setIsExporting ( false ) ;
136+ } , 10000 ) ;
137+
138+ try {
139+ const client = new ZipCaseClient ( ) ;
140+ await client . cases . export ( caseNumbers ) ;
141+ } catch ( error ) {
142+ console . error ( 'Export failed:' , error ) ;
143+ } finally {
144+ clearTimeout ( timeoutId ) ;
145+ setIsExporting ( false ) ;
146+ }
147+ } ;
148+
149+ const isExportEnabled = useMemo ( ( ) => {
150+ if ( searchResults . length === 0 ) return false ;
151+ const terminalStates = [ 'complete' , 'failed' , 'notFound' ] ;
152+ return searchResults . every ( r => terminalStates . includes ( r . zipCase . fetchStatus . status ) ) ;
153+ } , [ searchResults ] ) ;
154+
155+ const exportableCount = useMemo ( ( ) => {
156+ return searchResults . filter ( r => r . zipCase . fetchStatus . status !== 'notFound' ) . length ;
157+ } , [ searchResults ] ) ;
158+
123159 if ( isError ) {
124160 console . error ( 'Error in useSearchResults:' , error ) ;
125161 }
@@ -144,20 +180,66 @@ export default function SearchResultsList() {
144180 < div className = "mt-8" >
145181 < div className = "flex justify-between items-center" >
146182 < h3 className = "text-base font-semibold text-gray-900" > Search Results</ h3 >
147- < button
148- onClick = { copyCaseNumbers }
149- className = { `inline-flex items-center gap-x-2 rounded-md bg-white px-3 py-2 text-sm font-semibold shadow-sm ring-1 ring-inset hover:bg-gray-50 ${
150- copied ? 'text-green-700 ring-green-600' : 'text-gray-900 ring-gray-300'
151- } `}
152- aria-label = "Copy all case numbers"
153- >
154- { copied ? (
155- < CheckIcon className = "h-5 w-5 text-green-600" aria-hidden = "true" />
156- ) : (
157- < ClipboardDocumentIcon className = "h-5 w-5 text-gray-400" aria-hidden = "true" />
158- ) }
159- Copy Case Numbers
160- </ button >
183+ < div className = "flex gap-2" >
184+ < button
185+ type = "button"
186+ onClick = { handleExport }
187+ disabled = { ! isExportEnabled || isExporting }
188+ className = { `inline-flex items-center gap-x-1.5 rounded-md px-3 py-2 text-sm font-semibold shadow-sm ring-1 ring-inset ${
189+ isExportEnabled && ! isExporting
190+ ? 'bg-white text-gray-900 ring-gray-300 hover:bg-gray-50'
191+ : 'bg-gray-100 text-gray-400 ring-gray-200 cursor-not-allowed'
192+ } `}
193+ title = {
194+ isExportEnabled
195+ ? `Export ${ exportableCount } case${ exportableCount === 1 ? '' : 's' } `
196+ : 'Wait for all cases to finish processing before exporting'
197+ }
198+ >
199+ { isExporting ? (
200+ < svg
201+ className = "animate-spin -ml-0.5 h-5 w-5 text-gray-400"
202+ xmlns = "http://www.w3.org/2000/svg"
203+ fill = "none"
204+ viewBox = "0 0 24 24"
205+ >
206+ < circle
207+ className = "opacity-25"
208+ cx = "12"
209+ cy = "12"
210+ r = "10"
211+ stroke = "currentColor"
212+ strokeWidth = "4"
213+ > </ circle >
214+ < path
215+ className = "opacity-75"
216+ fill = "currentColor"
217+ d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
218+ > </ path >
219+ </ svg >
220+ ) : (
221+ < ArrowDownTrayIcon
222+ className = { `-ml-0.5 h-5 w-5 ${ isExportEnabled ? 'text-gray-400' : 'text-gray-300' } ` }
223+ aria-hidden = "true"
224+ />
225+ ) }
226+ Export
227+ </ button >
228+ < button
229+ onClick = { copyCaseNumbers }
230+ className = { `inline-flex items-center gap-x-2 rounded-md bg-white px-3 py-2 text-sm font-semibold shadow-sm ring-1 ring-inset hover:bg-gray-50 ${
231+ copied ? 'text-green-700 ring-green-600' : 'text-gray-900 ring-gray-300'
232+ } `}
233+ aria-label = "Copy all case numbers"
234+ >
235+ { copied ? (
236+ < CheckIcon className = "h-5 w-5 text-green-600" aria-hidden = "true" />
237+ ) : (
238+ < ClipboardDocumentIcon className = "h-5 w-5 text-gray-400" aria-hidden = "true" />
239+ ) }
240+ Copy Case Numbers
241+ </ button >
242+ </ div >
161243 </ div >
162244 < div className = "mt-4" >
163245 { displayItems . map ( ( item , index ) => (
0 commit comments