1- import React from 'react' ;
1+ import React , { useState } from 'react' ;
22
3- import { Heading , Select , StatisticCard } from '@douglasneuroinformatics/libui/components' ;
3+ import { Dialog , Heading , Select , Spinner , StatisticCard } from '@douglasneuroinformatics/libui/components' ;
44import { useTheme , useTranslation } from '@douglasneuroinformatics/libui/hooks' ;
55import type { Theme } from '@douglasneuroinformatics/libui/hooks' ;
66import { ClipboardDocumentIcon , DocumentTextIcon , UserIcon , UsersIcon } from '@heroicons/react/24/solid' ;
77import type { AppSubjectName } from '@opendatacapture/schemas/core' ;
8- import { createFileRoute , redirect } from '@tanstack/react-router' ;
8+ import { createFileRoute , redirect , useNavigate } from '@tanstack/react-router' ;
9+ import { AnimatePresence , motion } from 'motion/react' ;
910import { Area , AreaChart , CartesianGrid , ResponsiveContainer , Tooltip , XAxis , YAxis } from 'recharts' ;
1011
1112import { PageHeader } from '@/components/PageHeader' ;
13+ import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery' ;
14+ import { useInstrumentRecords } from '@/hooks/useInstrumentRecords' ;
1215import { summaryQueryOptions , useSummaryQuery } from '@/hooks/useSummaryQuery' ;
16+ import { useUsersQuery } from '@/hooks/useUsersQuery' ;
1317import { useAppStore } from '@/store' ;
1418
1519const RouteComponent = ( ) => {
@@ -19,6 +23,44 @@ const RouteComponent = () => {
1923 const { t } = useTranslation ( ) ;
2024 const [ theme ] = useTheme ( ) ;
2125 const summaryQuery = useSummaryQuery ( { params : { groupId : currentGroup ?. id } } ) ;
26+ const navigate = useNavigate ( ) ;
27+ const [ isInstrumentModalOpen , setIsInstrumentModalOpen ] = useState ( false ) ;
28+ const [ isUserModalOpen , setIsUserModalOpen ] = useState ( false ) ;
29+ const [ isRecordModalOpen , setIsRecordModalOpen ] = useState ( false ) ;
30+ const instrumentInfoQuery = useInstrumentInfoQuery ( ) ;
31+ const userInfoQuery = useUsersQuery ( ) ;
32+
33+ const recordsQuery = useInstrumentRecords ( {
34+ enabled : true ,
35+ params : {
36+ groupId : currentGroup ?. id
37+ }
38+ } ) ;
39+
40+ const instrumentData = currentGroup
41+ ? instrumentInfoQuery . data ?. filter ( ( instrument ) => {
42+ return currentGroup . accessibleInstrumentIds . includes ( instrument . id ) ;
43+ } )
44+ : instrumentInfoQuery . data ;
45+
46+ const instrumentInfo = instrumentData ?. map ( ( instrument ) => {
47+ return {
48+ id : instrument . id ,
49+ kind : instrument . kind ,
50+ title : instrument . details . title
51+ } ;
52+ } ) ;
53+
54+ const recordIds = recordsQuery . data ?. map ( ( record ) => record . instrumentId ) ;
55+
56+ const recordCounter =
57+ instrumentInfo ?. map ( ( title ) => {
58+ return {
59+ id : title . id ,
60+ count : recordIds ?. filter ( ( val ) => val === title . id ) . length ?? 0 ,
61+ instrumentTitle : title . title
62+ } ;
63+ } ) ?? [ ] ;
2264
2365 const chartColors = {
2466 records : {
@@ -133,23 +175,76 @@ const RouteComponent = () => {
133175 </ div >
134176 < div className = "body-font" data-testid = "dashboard-statistics" >
135177 < div className = "grid grid-cols-1 gap-6 text-center lg:grid-cols-2 xl:grid-cols-4" >
136- < div className = "group transform transition-all duration-300 hover:scale-105" data-testid = "statistic-users" >
137- < StatisticCard
138- icon = {
139- < UsersIcon className = "h-12 w-12 text-blue-600 transition-transform duration-300 group-hover:scale-110 dark:text-blue-400" />
140- }
141- label = { t ( {
142- en : 'Total Users' ,
143- fr : "Nombre d'utilisateurs"
144- } ) }
145- value = { summaryQuery . data . counts . users }
146- />
147- </ div >
148178 < div
149- className = "group transform transition-all duration-300 hover:scale-105"
179+ className = "group flex transform transition-all duration-300 hover:scale-105"
180+ data-testid = "statistic-users"
181+ >
182+ < Dialog open = { isUserModalOpen } onOpenChange = { setIsUserModalOpen } >
183+ < Dialog . Trigger className = "grow" >
184+ < StatisticCard
185+ icon = {
186+ < UsersIcon className = "h-12 w-12 text-blue-600 transition-transform duration-300 group-hover:scale-110 dark:text-blue-400" />
187+ }
188+ label = { t ( {
189+ en : 'Total Users' ,
190+ fr : "Nombre d'utilisateurs"
191+ } ) }
192+ value = { summaryQuery . data . counts . users }
193+ />
194+ </ Dialog . Trigger >
195+ < Dialog . Content data-spotlight-type = "users-Modal-modal" data-testid = "dashboard-users-Modal-dialog" >
196+ < Dialog . Header >
197+ < Dialog . Title >
198+ { t ( {
199+ en : 'Users' ,
200+ fr : 'Les utilisateurs'
201+ } ) }
202+ </ Dialog . Title >
203+ </ Dialog . Header >
204+ < hr > </ hr >
205+ < ul className = "flex flex-col gap-5 overflow-auto" >
206+ { userInfoQuery . isLoading && < Spinner /> }
207+ { userInfoQuery . isError && (
208+ < p >
209+ { t ( {
210+ en : 'Error finding users' ,
211+ fr : "erreur lors de la recherche d'utilisateurs"
212+ } ) }
213+ </ p >
214+ ) }
215+ < AnimatePresence mode = "popLayout" >
216+ { userInfoQuery . data ?. map ( ( user , i ) => {
217+ return (
218+ < motion . li
219+ layout
220+ animate = { { opacity : 1 , y : 0 } }
221+ exit = { { opacity : 0 } }
222+ initial = { { opacity : 0 } }
223+ key = { user . username }
224+ transition = { { bounce : 0.2 , delay : 0.15 * i , duration : 1.5 , type : 'spring' } }
225+ >
226+ < div className = "flex justify-between gap-4" >
227+ < p > { user . username } </ p >
228+ </ div >
229+ </ motion . li >
230+ ) ;
231+ } ) }
232+ </ AnimatePresence >
233+ </ ul >
234+ </ Dialog . Content >
235+ </ Dialog >
236+ </ div >
237+ < button
238+ className = "group flex transform transition-all duration-300 hover:scale-105"
150239 data-testid = "statistic-subjects"
240+ onClick = { ( ) => {
241+ void navigate ( {
242+ to : '/datahub'
243+ } ) ;
244+ } }
151245 >
152246 < StatisticCard
247+ className = "grow"
153248 icon = {
154249 < UserIcon className = "h-12 w-12 text-emerald-600 transition-transform duration-300 group-hover:scale-110 dark:text-emerald-400" />
155250 }
@@ -159,36 +254,127 @@ const RouteComponent = () => {
159254 } ) }
160255 value = { summaryQuery . data . counts . subjects }
161256 />
162- </ div >
257+ </ button >
163258 < div
164- className = "group transform transition-all duration-300 hover:scale-105"
259+ className = "group flex transform transition-all duration-300 hover:scale-105"
165260 data-testid = "statistic-instruments"
166261 >
167- < StatisticCard
168- icon = {
169- < ClipboardDocumentIcon className = "h-12 w-12 text-amber-600 transition-transform duration-300 group-hover:scale-110 dark:text-amber-400" />
170- }
171- label = { t ( {
172- en : 'Total Instruments' ,
173- fr : "Nombre d'instruments"
174- } ) }
175- value = { summaryQuery . data . counts . instruments }
176- />
262+ < Dialog open = { isInstrumentModalOpen } onOpenChange = { setIsInstrumentModalOpen } >
263+ < Dialog . Trigger className = "grow" >
264+ < StatisticCard
265+ icon = {
266+ < ClipboardDocumentIcon className = "h-12 w-12 text-amber-600 transition-transform duration-300 group-hover:scale-110 dark:text-amber-400" />
267+ }
268+ label = { t ( {
269+ en : 'Total Instruments' ,
270+ fr : "Nombre d'instruments"
271+ } ) }
272+ value = { summaryQuery . data . counts . instruments }
273+ > </ StatisticCard >
274+ </ Dialog . Trigger >
275+ < Dialog . Content
276+ data-spotlight-type = "instruments-Modal-modal"
277+ data-testid = "dashboard-instruments-Modal-dialog"
278+ >
279+ < Dialog . Header >
280+ < Dialog . Title >
281+ { t ( {
282+ en : 'Available Instruments' ,
283+ fr : 'Les instruments'
284+ } ) }
285+ </ Dialog . Title >
286+ </ Dialog . Header >
287+ < ul className = "flex flex-col gap-5 overflow-auto" >
288+ < AnimatePresence mode = "popLayout" >
289+ < div className = "flex justify-between gap-4 font-bold" >
290+ < p >
291+ { t ( {
292+ en : 'Title' ,
293+ fr : 'Titre'
294+ } ) }
295+ </ p > { ' ' }
296+ < p > { t ( { en : 'Kind' , fr : 'Genre' } ) } </ p >
297+ </ div >
298+ < hr > </ hr >
299+ { instrumentInfo ?. map ( ( instrument , i ) => {
300+ return (
301+ < motion . li
302+ layout
303+ animate = { { opacity : 1 , y : 0 } }
304+ exit = { { opacity : 0 } }
305+ initial = { { opacity : 0 } }
306+ key = { instrument . id }
307+ transition = { { bounce : 0.2 , delay : 0.15 * i , duration : 1.5 , type : 'spring' } }
308+ >
309+ < div className = "flex justify-between gap-4" >
310+ < p > { instrument . title } </ p > < p > { instrument . kind } </ p >
311+ </ div >
312+ </ motion . li >
313+ ) ;
314+ } ) }
315+ </ AnimatePresence >
316+ </ ul >
317+ </ Dialog . Content >
318+ </ Dialog >
177319 </ div >
178320 < div
179- className = "group transform transition-all duration-300 hover:scale-105"
321+ className = "group flex transform transition-all duration-300 hover:scale-105"
180322 data-testid = "statistic-records"
181323 >
182- < StatisticCard
183- icon = {
184- < DocumentTextIcon className = "h-12 w-12 text-purple-600 transition-transform duration-300 group-hover:scale-110 dark:text-purple-400" />
185- }
186- label = { t ( {
187- en : 'Total Records' ,
188- fr : "Nombre d'enregistrements"
189- } ) }
190- value = { summaryQuery . data . counts . records }
191- />
324+ < Dialog open = { isRecordModalOpen } onOpenChange = { setIsRecordModalOpen } >
325+ < Dialog . Trigger className = "grow" >
326+ < StatisticCard
327+ icon = {
328+ < DocumentTextIcon className = "h-12 w-12 text-purple-600 transition-transform duration-300 group-hover:scale-110 dark:text-purple-400" />
329+ }
330+ label = { t ( {
331+ en : 'Total Records' ,
332+ fr : "Nombre d'enregistrements"
333+ } ) }
334+ value = { summaryQuery . data . counts . records }
335+ />
336+ </ Dialog . Trigger >
337+ < Dialog . Content data-spotlight-type = "record-Modal-modal" data-testid = "dashboard-record-Modal-dialog" >
338+ < Dialog . Header >
339+ < Dialog . Title >
340+ { t ( {
341+ en : 'Number of Records' ,
342+ fr : "Nombre d'enregistrements"
343+ } ) }
344+ </ Dialog . Title >
345+ </ Dialog . Header >
346+ < ul className = "flex flex-col gap-5 overflow-auto" >
347+ < AnimatePresence mode = "popLayout" >
348+ < div className = "flex justify-between gap-4 font-bold" >
349+ < p >
350+ { t ( {
351+ en : 'Title' ,
352+ fr : 'Titre'
353+ } ) }
354+ </ p > { ' ' }
355+ < p > { t ( { en : 'Number' , fr : 'Numero' } ) } </ p >
356+ </ div >
357+ < hr > </ hr >
358+ { recordCounter ?. map ( ( instrument , i ) => {
359+ return (
360+ < motion . li
361+ layout
362+ animate = { { opacity : 1 , y : 0 } }
363+ exit = { { opacity : 0 } }
364+ initial = { { opacity : 0 } }
365+ key = { instrument . instrumentTitle }
366+ transition = { { bounce : 0.2 , delay : 0.15 * i , duration : 1.5 , type : 'spring' } }
367+ >
368+ < div className = "flex justify-between gap-4" >
369+ < p > { instrument . instrumentTitle } </ p > < p > { instrument . count } </ p >
370+ </ div >
371+ </ motion . li >
372+ ) ;
373+ } ) }
374+ </ AnimatePresence >
375+ </ ul >
376+ </ Dialog . Content >
377+ </ Dialog >
192378 </ div >
193379 </ div >
194380 </ div >
0 commit comments