From d2f899bd5d4ecbab2cd7055edda40d1eedbb46a5 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 17 Dec 2025 14:34:58 -0500 Subject: [PATCH 1/5] add dynamic list of instruments, navigate to datahub onclick of subjects card --- apps/web/src/routes/_app/dashboard.tsx | 96 ++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/apps/web/src/routes/_app/dashboard.tsx b/apps/web/src/routes/_app/dashboard.tsx index 0cfd37b9b..a6fab2f5a 100644 --- a/apps/web/src/routes/_app/dashboard.tsx +++ b/apps/web/src/routes/_app/dashboard.tsx @@ -1,14 +1,16 @@ -import React from 'react'; +import React, { useState } from 'react'; -import { Heading, Select, StatisticCard } from '@douglasneuroinformatics/libui/components'; +import { Dialog, Heading, Select, StatisticCard } from '@douglasneuroinformatics/libui/components'; import { useTheme, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { Theme } from '@douglasneuroinformatics/libui/hooks'; import { ClipboardDocumentIcon, DocumentTextIcon, UserIcon, UsersIcon } from '@heroicons/react/24/solid'; import type { AppSubjectName } from '@opendatacapture/schemas/core'; -import { createFileRoute, redirect } from '@tanstack/react-router'; +import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'; +import { AnimatePresence, motion } from 'motion/react'; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { PageHeader } from '@/components/PageHeader'; +import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { summaryQueryOptions, useSummaryQuery } from '@/hooks/useSummaryQuery'; import { useAppStore } from '@/store'; @@ -19,6 +21,9 @@ const RouteComponent = () => { const { t } = useTranslation(); const [theme] = useTheme(); const summaryQuery = useSummaryQuery({ params: { groupId: currentGroup?.id } }); + const navigate = useNavigate(); + const [isLookupOpen, setIsLookupOpen] = useState(false); + const instrumentInfoQuery = useInstrumentInfoQuery(); const chartColors = { records: { @@ -90,6 +95,19 @@ const RouteComponent = () => { }); } + const instrumentData = currentGroup + ? instrumentInfoQuery.data?.filter((instrument) => { + return currentGroup.accessibleInstrumentIds.includes(instrument.id); + }) + : instrumentInfoQuery.data; + + const instrumentInfo = instrumentData?.map((record) => { + return { + kind: record.kind, + title: record.details.title + }; + }); + // should never happen, as data is ensured in loader, but avoid crashing the app if someone changes this if (!summaryQuery.data) { return null; @@ -145,9 +163,14 @@ const RouteComponent = () => { value={summaryQuery.data.counts.users} /> -
{ + void navigate({ + to: '/datahub' + }); + }} > { })} value={summaryQuery.data.counts.subjects} /> -
+
- - } - label={t({ - en: 'Total Instruments', - fr: "Nombre d'instruments" - })} - value={summaryQuery.data.counts.instruments} - /> + + + + } + label={t({ + en: 'Total Instruments', + fr: "Nombre d'instruments" + })} + value={summaryQuery.data.counts.instruments} + > + + + + + {t({ + en: 'Available Instruments', + fr: 'Les instruments' + })} + + +
    + +
    +

    + {t({ + en: 'Title', + fr: 'Titre' + })} +

    {' '} +

    {t({ en: 'Kind' })}

    +
    + {instrumentInfo?.map((instrument, i) => { + return ( + +
    +

    {instrument.title}

    {instrument.kind}

    +
    +
    + ); + })} +
    +
+
+
Date: Fri, 19 Dec 2025 16:24:36 -0500 Subject: [PATCH 2/5] feat: add username list when clicking total users card, added flex to stat card divs --- apps/web/src/routes/_app/dashboard.tsx | 64 +++++++++++++++++++++----- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/apps/web/src/routes/_app/dashboard.tsx b/apps/web/src/routes/_app/dashboard.tsx index a6fab2f5a..90074dcda 100644 --- a/apps/web/src/routes/_app/dashboard.tsx +++ b/apps/web/src/routes/_app/dashboard.tsx @@ -13,6 +13,7 @@ import { PageHeader } from '@/components/PageHeader'; import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { summaryQueryOptions, useSummaryQuery } from '@/hooks/useSummaryQuery'; import { useAppStore } from '@/store'; +import { useUsersQuery } from '@/hooks/useUsersQuery'; const RouteComponent = () => { const changeGroup = useAppStore((store) => store.changeGroup); @@ -23,7 +24,9 @@ const RouteComponent = () => { const summaryQuery = useSummaryQuery({ params: { groupId: currentGroup?.id } }); const navigate = useNavigate(); const [isLookupOpen, setIsLookupOpen] = useState(false); + const [isUserLookupOpen, setIsUserLookupOpen] = useState(false); const instrumentInfoQuery = useInstrumentInfoQuery(); + const userInfoQuery = useUsersQuery(); const chartColors = { records: { @@ -151,17 +154,54 @@ const RouteComponent = () => {
-
- - } - label={t({ - en: 'Total Users', - fr: "Nombre d'utilisateurs" - })} - value={summaryQuery.data.counts.users} - /> +
+ + + + } + label={t({ + en: 'Total Users', + fr: "Nombre d'utilisateurs" + })} + value={summaryQuery.data.counts.users} + /> + + + + + {t({ + en: 'Users', + fr: 'Les utilisateurs' + })} + + +
    + + {userInfoQuery.data?.map((user, i) => { + return ( + +
    +

    {user.username}

    +
    +
    + ); + })} +
    +
+
+
From dfd62cb91dbf47cb2ef0138024df4b5cb04ba746 Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 23 Dec 2025 15:57:17 -0500 Subject: [PATCH 3/5] feat: add tanstack query to gather record information --- .../src/hooks/useInstrumentRecordExportQuery.ts | 16 ++++++++++++++++ apps/web/src/routes/_app/dashboard.tsx | 10 ++++++++++ 2 files changed, 26 insertions(+) create mode 100644 apps/web/src/hooks/useInstrumentRecordExportQuery.ts diff --git a/apps/web/src/hooks/useInstrumentRecordExportQuery.ts b/apps/web/src/hooks/useInstrumentRecordExportQuery.ts new file mode 100644 index 000000000..587d5012c --- /dev/null +++ b/apps/web/src/hooks/useInstrumentRecordExportQuery.ts @@ -0,0 +1,16 @@ +import type { InstrumentRecordsExport } from '@opendatacapture/schemas/instrument-records'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +export const useInstrumentRecordsExportQuery = (groupId?: string) => { + return useQuery({ + queryKey: ['instrument-records-export', groupId], + enabled: !!groupId, + queryFn: async () => { + const { data } = await axios.get('/v1/instrument-records/export', { + params: { groupId } + }); + return data; + } + }); +}; diff --git a/apps/web/src/routes/_app/dashboard.tsx b/apps/web/src/routes/_app/dashboard.tsx index 90074dcda..abe753ac4 100644 --- a/apps/web/src/routes/_app/dashboard.tsx +++ b/apps/web/src/routes/_app/dashboard.tsx @@ -14,6 +14,9 @@ import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { summaryQueryOptions, useSummaryQuery } from '@/hooks/useSummaryQuery'; import { useAppStore } from '@/store'; import { useUsersQuery } from '@/hooks/useUsersQuery'; +import type { InstrumentRecordsExport } from '@opendatacapture/schemas/instrument-records'; +import axios from 'axios'; +import { useInstrumentRecordsExportQuery } from '@/hooks/useInstrumentRecordExportQuery'; const RouteComponent = () => { const changeGroup = useAppStore((store) => store.changeGroup); @@ -28,6 +31,13 @@ const RouteComponent = () => { const instrumentInfoQuery = useInstrumentInfoQuery(); const userInfoQuery = useUsersQuery(); + const recordsExportQuery = useInstrumentRecordsExportQuery(currentGroup?.id); + + const recordsData = + recordsExportQuery.data?.map((record) => ({ + title: record.instrumentName + })) ?? []; + const chartColors = { records: { fill: theme === 'dark' ? 'rgba(59, 130, 246, 0.1)' : 'rgba(37, 99, 235, 0.1)', From e809baa6619251841568bfe3e52c436af8149a83 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 24 Dec 2025 12:55:14 -0500 Subject: [PATCH 4/5] feat: find record counts for all instruments --- .../hooks/useInstrumentRecordExportQuery.ts | 16 ------ apps/web/src/routes/_app/dashboard.tsx | 51 +++++++++++-------- 2 files changed, 30 insertions(+), 37 deletions(-) delete mode 100644 apps/web/src/hooks/useInstrumentRecordExportQuery.ts diff --git a/apps/web/src/hooks/useInstrumentRecordExportQuery.ts b/apps/web/src/hooks/useInstrumentRecordExportQuery.ts deleted file mode 100644 index 587d5012c..000000000 --- a/apps/web/src/hooks/useInstrumentRecordExportQuery.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { InstrumentRecordsExport } from '@opendatacapture/schemas/instrument-records'; -import { useQuery } from '@tanstack/react-query'; -import axios from 'axios'; - -export const useInstrumentRecordsExportQuery = (groupId?: string) => { - return useQuery({ - queryKey: ['instrument-records-export', groupId], - enabled: !!groupId, - queryFn: async () => { - const { data } = await axios.get('/v1/instrument-records/export', { - params: { groupId } - }); - return data; - } - }); -}; diff --git a/apps/web/src/routes/_app/dashboard.tsx b/apps/web/src/routes/_app/dashboard.tsx index abe753ac4..bc9242e0a 100644 --- a/apps/web/src/routes/_app/dashboard.tsx +++ b/apps/web/src/routes/_app/dashboard.tsx @@ -14,9 +14,7 @@ import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { summaryQueryOptions, useSummaryQuery } from '@/hooks/useSummaryQuery'; import { useAppStore } from '@/store'; import { useUsersQuery } from '@/hooks/useUsersQuery'; -import type { InstrumentRecordsExport } from '@opendatacapture/schemas/instrument-records'; -import axios from 'axios'; -import { useInstrumentRecordsExportQuery } from '@/hooks/useInstrumentRecordExportQuery'; +import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; const RouteComponent = () => { const changeGroup = useAppStore((store) => store.changeGroup); @@ -31,12 +29,36 @@ const RouteComponent = () => { const instrumentInfoQuery = useInstrumentInfoQuery(); const userInfoQuery = useUsersQuery(); - const recordsExportQuery = useInstrumentRecordsExportQuery(currentGroup?.id); + const recordsQuery = useInstrumentRecords({ + enabled: true, + params: { + groupId: currentGroup?.id + } + }); + + const instrumentData = currentGroup + ? instrumentInfoQuery.data?.filter((instrument) => { + return currentGroup.accessibleInstrumentIds.includes(instrument.id); + }) + : instrumentInfoQuery.data; + + const instrumentInfo = instrumentData?.map((instrument) => { + return { + kind: instrument.kind, + title: instrument.details.title, + id: instrument.id + }; + }); - const recordsData = - recordsExportQuery.data?.map((record) => ({ - title: record.instrumentName - })) ?? []; + const recordIds = recordsQuery.data?.map((record) => record.instrumentId); + + const recordCounter = + instrumentInfo?.map((title) => { + return { + instrumentTitle: title.title, + count: recordIds?.filter((val) => val === title.id).length ?? 0 + }; + }) ?? []; const chartColors = { records: { @@ -108,19 +130,6 @@ const RouteComponent = () => { }); } - const instrumentData = currentGroup - ? instrumentInfoQuery.data?.filter((instrument) => { - return currentGroup.accessibleInstrumentIds.includes(instrument.id); - }) - : instrumentInfoQuery.data; - - const instrumentInfo = instrumentData?.map((record) => { - return { - kind: record.kind, - title: record.details.title - }; - }); - // should never happen, as data is ensured in loader, but avoid crashing the app if someone changes this if (!summaryQuery.data) { return null; From e3e5f6ed0f68c1e24bc513383cb06344f57ff85b Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 24 Dec 2025 14:45:09 -0500 Subject: [PATCH 5/5] feat: add interactive box to display records for each instrument --- apps/web/src/routes/_app/dashboard.tsx | 71 +++++++++++++++++++++----- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/apps/web/src/routes/_app/dashboard.tsx b/apps/web/src/routes/_app/dashboard.tsx index bc9242e0a..408b9b045 100644 --- a/apps/web/src/routes/_app/dashboard.tsx +++ b/apps/web/src/routes/_app/dashboard.tsx @@ -26,6 +26,7 @@ const RouteComponent = () => { const navigate = useNavigate(); const [isLookupOpen, setIsLookupOpen] = useState(false); const [isUserLookupOpen, setIsUserLookupOpen] = useState(false); + const [isRecordLookupOpen, setIsRecordLookupOpen] = useState(false); const instrumentInfoQuery = useInstrumentInfoQuery(); const userInfoQuery = useUsersQuery(); @@ -223,7 +224,7 @@ const RouteComponent = () => {
{instrumentInfo?.map((instrument, i) => { return ( @@ -301,19 +303,62 @@ const RouteComponent = () => {
- - } - label={t({ - en: 'Total Records', - fr: "Nombre d'enregistrements" - })} - value={summaryQuery.data.counts.records} - /> + + + + } + label={t({ + en: 'Total Records', + fr: "Nombre d'enregistrements" + })} + value={summaryQuery.data.counts.records} + /> + + + + + {t({ + en: 'Number of Records', + fr: "Nombre d'enregistrements" + })} + + +
    + +
    +

    + {t({ + en: 'Title', + fr: 'Titre' + })} +

    {' '} +

    {t({ en: 'Number', fr: 'Numero' })}

    +
    + {recordCounter?.map((instrument, i) => { + return ( + +
    +

    {instrument.instrumentTitle}

    {instrument.count}

    +
    +
    + ); + })} +
    +
+
+