@@ -29,12 +29,20 @@ import {
2929 TableRow ,
3030 Text ,
3131} from '@trycompai/design-system' ;
32- import { Add , Catalog , Download , Search } from '@trycompai/design-system/icons' ;
32+ import { Add , Catalog , Download , Search , Upload } from '@trycompai/design-system/icons' ;
33+ import {
34+ Dialog ,
35+ DialogContent ,
36+ DialogDescription ,
37+ DialogFooter ,
38+ DialogHeader ,
39+ DialogTitle ,
40+ } from '@comp/ui/dialog' ;
3341import Link from 'next/link' ;
3442import { useRouter } from 'next/navigation' ;
35- import { useMemo , useState } from 'react' ;
43+ import { useCallback , useMemo , useRef , useState } from 'react' ;
3644import { toast } from 'sonner' ;
37- import useSWR from 'swr' ;
45+ import useSWR , { useSWRConfig } from 'swr' ;
3846import { DocumentFindingsSection } from './DocumentFindingsSection' ;
3947import { StatusBadge , formatSubmissionDate } from './submission-utils' ;
4048
@@ -84,6 +92,19 @@ function getMatrixRowCount(value: unknown): number {
8492 return value . filter ( ( row ) => row && typeof row === 'object' ) . length ;
8593}
8694
95+ function fileToBase64 ( file : File ) : Promise < string > {
96+ return new Promise ( ( resolve , reject ) => {
97+ const reader = new FileReader ( ) ;
98+ reader . onload = ( ) => {
99+ const result = reader . result as string ;
100+ const base64 = result . split ( ',' ) [ 1 ] ?? '' ;
101+ resolve ( base64 ) ;
102+ } ;
103+ reader . onerror = reject ;
104+ reader . readAsDataURL ( file ) ;
105+ } ) ;
106+ }
107+
87108async function evidenceFormFetcher ( [ endpoint , orgId ] : readonly [
88109 string ,
89110 string ,
@@ -107,8 +128,13 @@ export function CompanyFormPageClient({
107128 isPlatformAdmin : boolean ;
108129} ) {
109130 const router = useRouter ( ) ;
131+ const { mutate : globalMutate } = useSWRConfig ( ) ;
110132 const [ search , setSearch ] = useState ( '' ) ;
111133 const [ isExporting , setIsExporting ] = useState ( false ) ;
134+ const [ isUploadOpen , setIsUploadOpen ] = useState ( false ) ;
135+ const [ isUploading , setIsUploading ] = useState ( false ) ;
136+ const [ selectedFile , setSelectedFile ] = useState < File | null > ( null ) ;
137+ const fileInputRef = useRef < HTMLInputElement > ( null ) ;
112138
113139 const { data : activeMember } = useActiveMember ( ) ;
114140 const memberRoles = activeMember ?. role ?. split ( ',' ) . map ( ( role : string ) => role . trim ( ) ) || [ ] ;
@@ -248,6 +274,47 @@ export function CompanyFormPageClient({
248274 }
249275 } ;
250276
277+ const handleUploadEvidence = useCallback ( async ( ) => {
278+ if ( ! selectedFile ) return ;
279+
280+ setIsUploading ( true ) ;
281+ try {
282+ const fileData = await fileToBase64 ( selectedFile ) ;
283+ const submitFormType = isMeeting ? MEETING_SUB_TYPES [ 0 ] : formType ;
284+
285+ const response = await api . post (
286+ `/v1/evidence-forms/${ submitFormType } /upload-submission` ,
287+ {
288+ fileName : selectedFile . name ,
289+ fileType : selectedFile . type || 'application/octet-stream' ,
290+ fileData,
291+ } ,
292+ organizationId ,
293+ ) ;
294+
295+ if ( response . error ) {
296+ throw new Error ( response . error ) ;
297+ }
298+
299+ toast . success ( 'Evidence uploaded' ) ;
300+ setIsUploadOpen ( false ) ;
301+ setSelectedFile ( null ) ;
302+ if ( fileInputRef . current ) fileInputRef . current . value = '' ;
303+
304+ if ( isMeeting ) {
305+ for ( const subType of MEETING_SUB_TYPES ) {
306+ globalMutate ( [ `/v1/evidence-forms/${ subType } ${ query } ` , organizationId ] ) ;
307+ }
308+ } else {
309+ globalMutate ( [ `/v1/evidence-forms/${ formType } ${ query } ` , organizationId ] ) ;
310+ }
311+ } catch ( error ) {
312+ toast . error ( error instanceof Error ? error . message : 'Upload failed' ) ;
313+ } finally {
314+ setIsUploading ( false ) ;
315+ }
316+ } , [ selectedFile , isMeeting , formType , organizationId , query , globalMutate ] ) ;
317+
251318 return (
252319 < div className = "space-y-6" >
253320 < PageHeader
@@ -257,6 +324,14 @@ export function CompanyFormPageClient({
257324 < Link href = { `/${ organizationId } /documents/${ formType } /new` } >
258325 < Button iconLeft = { < Add size = { 16 } /> } > New Submission</ Button >
259326 </ Link >
327+ < Button
328+ type = "button"
329+ variant = "secondary"
330+ iconLeft = { < Upload size = { 16 } /> }
331+ onClick = { ( ) => setIsUploadOpen ( true ) }
332+ >
333+ Upload Evidence
334+ </ Button >
260335 < Button
261336 type = "button"
262337 variant = "secondary"
@@ -414,6 +489,58 @@ export function CompanyFormPageClient({
414489 isPlatformAdmin = { isPlatformAdmin }
415490 isAdminOrOwner = { isAdminOrOwner }
416491 />
492+
493+ < Dialog
494+ open = { isUploadOpen }
495+ onOpenChange = { ( open ) => {
496+ setIsUploadOpen ( open ) ;
497+ if ( ! open ) {
498+ setSelectedFile ( null ) ;
499+ if ( fileInputRef . current ) fileInputRef . current . value = '' ;
500+ }
501+ } }
502+ >
503+ < DialogContent >
504+ < DialogHeader >
505+ < DialogTitle > Upload Evidence</ DialogTitle >
506+ < DialogDescription >
507+ Upload a PDF or image as evidence for this document.
508+ </ DialogDescription >
509+ </ DialogHeader >
510+ < div className = "space-y-4" >
511+ < input
512+ ref = { fileInputRef }
513+ type = "file"
514+ accept = ".pdf,.png,.jpg,.jpeg"
515+ onChange = { ( e ) => setSelectedFile ( e . target . files ?. [ 0 ] ?? null ) }
516+ className = "block w-full text-sm text-foreground file:mr-4 file:rounded-md file:border-0 file:bg-muted file:px-4 file:py-2 file:text-sm file:font-medium file:text-foreground hover:file:bg-muted/80 file:cursor-pointer cursor-pointer"
517+ />
518+ { selectedFile && (
519+ < Text size = "sm" variant = "muted" >
520+ { selectedFile . name } ({ ( selectedFile . size / 1024 ) . toFixed ( 1 ) } KB)
521+ </ Text >
522+ ) }
523+ </ div >
524+ < DialogFooter >
525+ < Button
526+ type = "button"
527+ variant = "secondary"
528+ onClick = { ( ) => setIsUploadOpen ( false ) }
529+ disabled = { isUploading }
530+ >
531+ Cancel
532+ </ Button >
533+ < Button
534+ type = "button"
535+ onClick = { handleUploadEvidence }
536+ disabled = { ! selectedFile || isUploading }
537+ loading = { isUploading }
538+ >
539+ Upload
540+ </ Button >
541+ </ DialogFooter >
542+ </ DialogContent >
543+ </ Dialog >
417544 </ div >
418545 ) ;
419546}
0 commit comments