11import React , { useState } from 'react' ;
22
3- import { isZodType } from '@douglasneuroinformatics/libjs' ;
3+ import { serializeError } from '@douglasneuroinformatics/libjs' ;
44import { Button , FileDropzone , Heading , Spinner } from '@douglasneuroinformatics/libui/components' ;
55import { useDownload , useNotificationsStore , useTranslation } from '@douglasneuroinformatics/libui/hooks' ;
66import type { AnyUnilingualFormInstrument } from '@opendatacapture/runtime-core' ;
77import { createFileRoute } from '@tanstack/react-router' ;
8- import { BadgeHelpIcon , CircleAlertIcon , DownloadIcon } from 'lucide-react' ;
8+ import { BadgeHelpIcon , DownloadIcon } from 'lucide-react' ;
9+ import z from 'zod/v4' ;
910
1011import { PageHeader } from '@/components/PageHeader' ;
1112import { useInstrument } from '@/hooks/useInstrument' ;
1213import { useUploadInstrumentRecordsMutation } from '@/hooks/useUploadInstrumentRecordsMutation' ;
1314import { useAppStore } from '@/store' ;
14- import { createUploadTemplateCSV , processInstrumentCSV , reformatInstrumentData } from '@/utils/upload' ;
15+ import { createUploadTemplateCSV , processInstrumentCSV , reformatInstrumentData , UploadError } from '@/utils/upload' ;
1516
1617const RouteComponent = ( ) => {
1718 const [ file , setFile ] = useState < File | null > ( null ) ;
@@ -22,76 +23,109 @@ const RouteComponent = () => {
2223 const uploadInstrumentRecordsMutation = useUploadInstrumentRecordsMutation ( ) ;
2324
2425 const params = Route . useParams ( ) ;
26+ const { error } = Route . useSearch ( ) ;
27+ const navigate = Route . useNavigate ( ) ;
28+
2529 const instrument = useInstrument ( params . instrumentId ) as ( AnyUnilingualFormInstrument & { id : string } ) | null ;
2630 const { t } = useTranslation ( ) ;
2731
2832 const handleTemplateDownload = ( ) => {
2933 try {
30- const { content, fileName } = createUploadTemplateCSV ( instrument ! ) ;
31- void download ( fileName , content ) ;
34+ const { content, filename } = createUploadTemplateCSV ( instrument ! ) ;
35+ void download ( filename , content ) ;
3236 } catch ( error ) {
33- if ( error instanceof Error ) {
34- addNotification ( {
35- message : t ( {
36- en : `Error occurred downloading sample template with the following message: ${ error . message } ` ,
37- fr : `Un occurence d'un erreur quand le csv document est telecharger avec la message suivant: ${ error . message } `
38- } ) ,
39- type : 'error'
40- } ) ;
41- } else {
42- addNotification ( {
43- message : t ( {
44- en : `Error occurred downloading sample template.` ,
45- fr : `Un occurence d'un erreur quand le csv est telecharger. `
46- } ) ,
47- type : 'error'
48- } ) ;
49- }
5037 console . error ( error ) ;
38+ void navigate ( {
39+ search : {
40+ error : {
41+ description : error instanceof UploadError ? error . description : undefined ,
42+ title : {
43+ en : `Error Occurred Downloading Sample Template` ,
44+ fr : `Une erreur s'est produite lors du téléchargement du CSV`
45+ }
46+ }
47+ } ,
48+ to : '.'
49+ } ) ;
5150 }
5251 } ;
5352
5453 const handleInstrumentCSV = async ( ) => {
5554 try {
5655 setIsLoading ( true ) ;
5756 const processedDataResult = await processInstrumentCSV ( file ! , instrument ! ) ;
58- if ( processedDataResult . success ) {
59- const reformattedData = reformatInstrumentData ( {
60- currentGroup,
61- data : processedDataResult . value ,
62- instrument : instrument !
63- } ) ;
64- if ( reformattedData . records . length > 1000 ) {
65- addNotification ( {
66- message : t ( {
67- en : 'Lots of entries loading, please wait...' ,
68- fr : 'Beaucoup de données, veuillez patienter...'
69- } ) ,
70- type : 'info'
71- } ) ;
72- }
73- await uploadInstrumentRecordsMutation . mutateAsync ( reformattedData ) ;
74- } else {
57+ const reformattedData = reformatInstrumentData ( {
58+ currentGroup,
59+ data : processedDataResult ,
60+ instrument : instrument !
61+ } ) ;
62+ if ( reformattedData . records . length > 1000 ) {
7563 addNotification ( {
76- message : processedDataResult . message ,
77- type : 'error'
64+ message : t ( {
65+ en : 'Lots of entries loading, please wait...' ,
66+ fr : 'Beaucoup de données, veuillez patienter...'
67+ } ) ,
68+ type : 'info'
7869 } ) ;
7970 }
71+ await uploadInstrumentRecordsMutation . mutateAsync ( reformattedData ) ;
8072 setFile ( null ) ;
8173 } catch ( error ) {
82- if ( error instanceof Error )
83- addNotification ( {
84- message : t ( {
85- en : `An error has happened within the request: '${ error . message } '` ,
86- fr : `Une erreur s'est produite lors du téléversement :'${ error . message } '.`
87- } ) ,
88- type : 'error'
89- } ) ;
74+ console . error ( error ) ;
75+ void navigate ( {
76+ search : {
77+ error : {
78+ description : error instanceof UploadError ? error . description : undefined ,
79+ title : {
80+ en : `An error has happened within the request` ,
81+ fr : `Une erreur s'est produite lors du téléversement`
82+ }
83+ }
84+ } ,
85+ to : '.'
86+ } ) ;
9087 } finally {
9188 setIsLoading ( false ) ;
9289 }
9390 } ;
9491
92+ if ( error ) {
93+ return (
94+ < div className = "flex min-h-screen flex-col items-center justify-center gap-1 p-3 text-center" >
95+ < h3 className = "text-2xl font-extrabold tracking-tight sm:text-3xl" > { t ( error . title ) } </ h3 >
96+ { error . description && (
97+ < p className = "text-muted-foreground mt-2 max-w-prose text-sm sm:text-base" > { t ( error . description ) } </ p >
98+ ) }
99+ < div className = "mt-6 flex gap-2" >
100+ < Button
101+ type = "button"
102+ variant = "outline"
103+ onClick = { ( ) => {
104+ void download ( 'error.json' , JSON . stringify ( serializeError ( error ) , null , 2 ) ) ;
105+ } }
106+ >
107+ { t ( {
108+ en : 'Error Report' ,
109+ fr : "Rapport d'erreur"
110+ } ) }
111+ </ Button >
112+ < Button
113+ type = "button"
114+ variant = "primary"
115+ onClick = { ( ) => {
116+ void navigate ( { to : '.' } ) ;
117+ } }
118+ >
119+ { t ( {
120+ en : 'Try Again' ,
121+ fr : 'Réessayer'
122+ } ) }
123+ </ Button >
124+ </ div >
125+ </ div >
126+ ) ;
127+ }
128+
95129 if ( ! instrument ) {
96130 return null ;
97131 }
@@ -107,56 +141,44 @@ const RouteComponent = () => {
107141 </ Heading >
108142 </ PageHeader >
109143 { ! isLoading ? (
110- isZodType ( instrument . validationSchema , { version : 4 } ) ? (
111- < div className = "mb-2 flex items-center gap-2 rounded-md bg-red-300 p-4 dark:bg-red-800" >
112- < CircleAlertIcon style = { { height : '20px' , strokeWidth : '2px' , width : '20px' } } />
113- < h5 className = "font-medium tracking-tight" >
114- { t ( {
115- en : 'Upload is Not Supported for Zod v4 Instruments' ,
116- fr : "Le téléchargement n'est pas pris en charge pour les instruments utilisant Zod v4"
117- } ) }
118- </ h5 >
119- </ div >
120- ) : (
121- < div className = "mx-auto flex w-full max-w-3xl grow flex-col justify-center" >
122- < FileDropzone
123- acceptedFileTypes = { {
124- 'text/csv' : [ '.csv' ]
125- } }
126- className = "flex h-80 w-full flex-col"
127- file = { file }
128- setFile = { setFile }
129- />
130- < div className = "mt-4 flex justify-between space-x-2" >
131- < Button disabled = { ! ( file && instrument ) } variant = { 'primary' } onClick = { ( ) => void handleInstrumentCSV ( ) } >
132- { t ( 'core.submit' ) }
144+ < div className = "mx-auto flex w-full max-w-3xl grow flex-col justify-center" >
145+ < FileDropzone
146+ acceptedFileTypes = { {
147+ 'text/csv' : [ '.csv' ]
148+ } }
149+ className = "flex h-80 w-full flex-col"
150+ file = { file }
151+ setFile = { setFile }
152+ />
153+ < div className = "mt-4 flex justify-between space-x-2" >
154+ < Button disabled = { ! ( file && instrument ) } variant = { 'primary' } onClick = { ( ) => void handleInstrumentCSV ( ) } >
155+ { t ( 'core.submit' ) }
156+ </ Button >
157+ < div className = "flex justify-between space-x-1" >
158+ < Button className = "gap-1" disabled = { ! instrument } variant = { 'primary' } onClick = { handleTemplateDownload } >
159+ < DownloadIcon />
160+ { t ( {
161+ en : 'Download Template' ,
162+ fr : 'Télécharger le modèle'
163+ } ) }
164+ </ Button >
165+ < Button
166+ className = "gap-1"
167+ disabled = { ! instrument }
168+ variant = { 'primary' }
169+ onClick = { ( ) => {
170+ window . open ( 'https://opendatacapture.org/en/docs/guides/how-to-upload-data/' ) ;
171+ } }
172+ >
173+ < BadgeHelpIcon />
174+ { t ( {
175+ en : 'Help' ,
176+ fr : 'Aide'
177+ } ) }
133178 </ Button >
134- < div className = "flex justify-between space-x-1" >
135- < Button className = "gap-1" disabled = { ! instrument } variant = { 'primary' } onClick = { handleTemplateDownload } >
136- < DownloadIcon />
137- { t ( {
138- en : 'Download Template' ,
139- fr : 'Télécharger le modèle'
140- } ) }
141- </ Button >
142- < Button
143- className = "gap-1"
144- disabled = { ! instrument }
145- variant = { 'primary' }
146- onClick = { ( ) => {
147- window . open ( 'https://opendatacapture.org/en/docs/guides/how-to-upload-data/' ) ;
148- } }
149- >
150- < BadgeHelpIcon />
151- { t ( {
152- en : 'Help' ,
153- fr : 'Aide'
154- } ) }
155- </ Button >
156- </ div >
157179 </ div >
158180 </ div >
159- )
181+ </ div >
160182 ) : (
161183 < >
162184 < div className = "mx-auto flex w-full max-w-3xl grow flex-col justify-center" >
@@ -175,5 +197,22 @@ const RouteComponent = () => {
175197} ;
176198
177199export const Route = createFileRoute ( '/_app/upload/$instrumentId' ) ( {
178- component : RouteComponent
200+ component : RouteComponent ,
201+ validateSearch : z . object ( {
202+ error : z
203+ . object ( {
204+ description : z
205+ . object ( {
206+ en : z . string ( ) ,
207+ fr : z . string ( )
208+ } )
209+ . partial ( )
210+ . optional ( ) ,
211+ title : z . object ( {
212+ en : z . string ( ) ,
213+ fr : z . string ( )
214+ } )
215+ } )
216+ . optional ( )
217+ } )
179218} ) ;
0 commit comments