From 8eb1304b8066d6b9dcb5ab96fd03d16b42232554 Mon Sep 17 00:00:00 2001 From: david-roper Date: Fri, 24 Oct 2025 16:57:26 -0400 Subject: [PATCH 01/86] feat: create useFindSession Hook --- apps/web/src/hooks/useFindSession.ts | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 apps/web/src/hooks/useFindSession.ts diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts new file mode 100644 index 000000000..b072e5cfb --- /dev/null +++ b/apps/web/src/hooks/useFindSession.ts @@ -0,0 +1,30 @@ +import { reviver } from '@douglasneuroinformatics/libjs'; +import { $Session } from '@opendatacapture/schemas/session'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +type UseSessionOptions = { + enabled?: boolean; + params: { + id?: string; + }; +}; + +export const useFindSession = ( + { enabled, params }: UseSessionOptions = { + enabled: true, + params: {} + } +) => { + return useQuery({ + enabled, + queryFn: async () => { + const response = await axios.get('/v1/Sessions', { + params, + transformResponse: [(data: string) => JSON.parse(data, reviver) as unknown] + }); + return $Session.array().parseAsync(response.data); + }, + queryKey: ['sessions', ...Object.values(params)] + }); +}; From a8417907ea1c700e8f3c335a6fff8168cf4b9b5f Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 27 Oct 2025 13:08:49 -0400 Subject: [PATCH 02/86] feat: make useSession hook return one session instead of array --- apps/web/src/hooks/useFindSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index b072e5cfb..8b5e5f5b3 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -23,7 +23,7 @@ export const useFindSession = ( params, transformResponse: [(data: string) => JSON.parse(data, reviver) as unknown] }); - return $Session.array().parseAsync(response.data); + return $Session.parseAsync(response.data); }, queryKey: ['sessions', ...Object.values(params)] }); From 39fc54a087b0d4c2a84d29cc747c36d0a6cb83f3 Mon Sep 17 00:00:00 2001 From: David Roper Date: Mon, 27 Oct 2025 14:52:05 -0400 Subject: [PATCH 03/86] feat: add hook to return list of user ids --- apps/web/src/hooks/useInstrumentVisualization.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 8246a7475..8e6cde440 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -12,6 +12,7 @@ import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; +import { useFindSession } from './useFindSession'; type InstrumentVisualizationRecord = { [key: string]: unknown; @@ -54,6 +55,19 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); + const usersQuery = recordsQuery.data?.map((item) => { + const sessionInfo = useFindSession({ + enabled: true, + params: { id: item.sessionId } + }); + if (sessionInfo.data) { + return sessionInfo.data.userId; + } + return 'N/A'; + }); + + console.log(usersQuery); + const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); From 0b74c71330f70fef6f68b524d768d3d7c4da2b79 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 27 Oct 2025 17:11:44 -0400 Subject: [PATCH 04/86] feat: new api reqs for finding sessions --- apps/api/src/sessions/sessions.controller.ts | 8 ++++++++ apps/api/src/sessions/sessions.service.ts | 15 +++++++++++++++ apps/web/src/hooks/useInstrumentVisualization.ts | 14 -------------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 47791d576..ef4fec5a5 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -26,4 +26,12 @@ export class SessionsController { findByID(@Param('id') id: string, @CurrentUser('ability') ability: AppAbility): Promise { return this.sessionsService.findById(id, { ability }); } + + @ApiOperation({ description: 'Find Session by ID' }) + @Post('list') + @RouteAccess({ action: 'read', subject: 'Session' }) + findSessionList(@Query('ids') ids: string[]): Promise { + const idArray = Array.isArray(ids) ? ids : (ids as string).split(','); + return this.sessionsService.findSessionList(idArray); + } } diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index 3d01b0fe1..ee84a55a7 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -104,6 +104,21 @@ export class SessionsService { return session; } + async findSessionList(ids: string[], { ability }: EntityOperationOptions = {}) { + const sessionsArray = await Promise.all( + ids.map(async (id) => { + const session = await this.sessionModel.findFirst({ + where: { AND: [accessibleQuery(ability, 'read', 'Session')], id } + }); + if (!session) { + throw new NotFoundException(`Failed to find session with ID: ${id}`); + } + return session; + }) + ); + return sessionsArray; + } + /** Get the subject if they exist, otherwise create them */ private async resolveSubject(subjectData: CreateSubjectData) { this.loggingService.debug({ message: 'Attempting to resolve subject', subjectData }); diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 8e6cde440..8246a7475 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -12,7 +12,6 @@ import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; -import { useFindSession } from './useFindSession'; type InstrumentVisualizationRecord = { [key: string]: unknown; @@ -55,19 +54,6 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); - const usersQuery = recordsQuery.data?.map((item) => { - const sessionInfo = useFindSession({ - enabled: true, - params: { id: item.sessionId } - }); - if (sessionInfo.data) { - return sessionInfo.data.userId; - } - return 'N/A'; - }); - - console.log(usersQuery); - const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); From 58b2a55c2983f8d7d4a342d857ccf74516e91641 Mon Sep 17 00:00:00 2001 From: David Roper Date: Thu, 6 Nov 2025 14:41:29 -0500 Subject: [PATCH 05/86] feat: add query from nest --- apps/api/src/sessions/sessions.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index ef4fec5a5..34d9f40ed 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -1,5 +1,5 @@ import { CurrentUser } from '@douglasneuroinformatics/libnest'; -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; import { ApiOperation } from '@nestjs/swagger'; import type { Session } from '@prisma/client'; From bfd64253fd00d9639b33bc51a2f6562618b60bc7 Mon Sep 17 00:00:00 2001 From: David Roper Date: Thu, 6 Nov 2025 16:41:43 -0500 Subject: [PATCH 06/86] feat: add userinfo method --- .../src/hooks/useInstrumentVisualization.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 8246a7475..ba1f88cce 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -4,6 +4,7 @@ import { toBasicISOString } from '@douglasneuroinformatics/libjs'; import { useDownload, useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { AnyUnilingualScalarInstrument, InstrumentKind } from '@opendatacapture/runtime-core'; import { removeSubjectIdScope } from '@opendatacapture/subject-utils'; +import axios from 'axios'; import { omit } from 'lodash-es'; import { unparse } from 'papaparse'; @@ -12,6 +13,7 @@ import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; +import type { Session } from '@opendatacapture/schemas/session'; type InstrumentVisualizationRecord = { [key: string]: unknown; @@ -54,6 +56,21 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); + const userInfo = async (sessionId: string) => { + const userData = await axios + .get(`/v1/sessions/${sessionId}`) + .then(function (response) { + if (response.data) { + return response.data as Session; + } + return null; + }) + .catch(function (error) { + console.error('Error fetching users:', error); + }); + return userData; + }; + const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); @@ -199,6 +216,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio const records: InstrumentVisualizationRecord[] = []; for (const record of recordsQuery.data) { const props = record.data && typeof record.data === 'object' ? record.data : {}; + const userData = userInfo(record.sessionId); records.push({ __date__: record.date, __time__: record.date.getTime(), From 0911db7eddeb65a18ae9e7ee0dc0ebb70667c584 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 10 Nov 2025 14:13:38 -0500 Subject: [PATCH 07/86] refactor: remove unused useFindSession hook --- apps/web/src/hooks/useFindSession.ts | 30 ---------------------------- 1 file changed, 30 deletions(-) delete mode 100644 apps/web/src/hooks/useFindSession.ts diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts deleted file mode 100644 index 8b5e5f5b3..000000000 --- a/apps/web/src/hooks/useFindSession.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { reviver } from '@douglasneuroinformatics/libjs'; -import { $Session } from '@opendatacapture/schemas/session'; -import { useQuery } from '@tanstack/react-query'; -import axios from 'axios'; - -type UseSessionOptions = { - enabled?: boolean; - params: { - id?: string; - }; -}; - -export const useFindSession = ( - { enabled, params }: UseSessionOptions = { - enabled: true, - params: {} - } -) => { - return useQuery({ - enabled, - queryFn: async () => { - const response = await axios.get('/v1/Sessions', { - params, - transformResponse: [(data: string) => JSON.parse(data, reviver) as unknown] - }); - return $Session.parseAsync(response.data); - }, - queryKey: ['sessions', ...Object.values(params)] - }); -}; From 814e882723998d8398279dbaddac8de0b2143fe4 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 10 Nov 2025 14:41:12 -0500 Subject: [PATCH 08/86] feat: add userInfo call to useEffect to get userId from the session --- .../src/hooks/useInstrumentVisualization.ts | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index ba1f88cce..e99e3daee 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react'; import { toBasicISOString } from '@douglasneuroinformatics/libjs'; import { useDownload, useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { AnyUnilingualScalarInstrument, InstrumentKind } from '@opendatacapture/runtime-core'; +import type { Session } from '@opendatacapture/schemas/session'; import { removeSubjectIdScope } from '@opendatacapture/subject-utils'; import axios from 'axios'; import { omit } from 'lodash-es'; @@ -13,7 +14,6 @@ import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; -import type { Session } from '@opendatacapture/schemas/session'; type InstrumentVisualizationRecord = { [key: string]: unknown; @@ -56,19 +56,14 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); - const userInfo = async (sessionId: string) => { - const userData = await axios - .get(`/v1/sessions/${sessionId}`) - .then(function (response) { - if (response.data) { - return response.data as Session; - } - return null; - }) - .catch(function (error) { - console.error('Error fetching users:', error); - }); - return userData; + const userInfo = async (sessionId: string): Promise => { + try { + const response = await axios.get(`/v1/sessions/${sessionId}`); + return response.data ? (response.data as Session) : null; + } catch (error) { + console.error('Error fetching user:', error); + return null; // ensures a resolved value instead of `void` + } }; const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { @@ -212,20 +207,38 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio }; useEffect(() => { - if (recordsQuery.data) { - const records: InstrumentVisualizationRecord[] = []; - for (const record of recordsQuery.data) { - const props = record.data && typeof record.data === 'object' ? record.data : {}; - const userData = userInfo(record.sessionId); - records.push({ - __date__: record.date, - __time__: record.date.getTime(), - ...record.computedMeasures, - ...props - }); + const fetchRecords = async () => { + if (recordsQuery.data) { + const records: InstrumentVisualizationRecord[] = []; + + for (const record of recordsQuery.data) { + const props = record.data && typeof record.data === 'object' ? record.data : {}; + + const userData = await userInfo(record.sessionId); + if (userData?.userId) { + // safely check since userData can be null + records.push({ + __date__: record.date, + __time__: record.date.getTime(), + username: userData.userId, + ...record.computedMeasures, + ...props + }); + continue; + } + records.push({ + __date__: record.date, + __time__: record.date.getTime(), + username: 'N/A', + ...record.computedMeasures, + ...props + }); + } + + setRecords(records); } - setRecords(records); - } + }; + void fetchRecords(); }, [recordsQuery.data]); const instrumentOptions: { [key: string]: string } = useMemo(() => { From 13f063d36511c71d1333ee86cd8e3164f0fbc6f5 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 10 Nov 2025 15:24:22 -0500 Subject: [PATCH 09/86] chore: rename user id column --- apps/web/src/hooks/useInstrumentVisualization.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index e99e3daee..98d0aa114 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -220,7 +220,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio records.push({ __date__: record.date, __time__: record.date.getTime(), - username: userData.userId, + userId: userData.userId, ...record.computedMeasures, ...props }); @@ -229,7 +229,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio records.push({ __date__: record.date, __time__: record.date.getTime(), - username: 'N/A', + userId: 'N/A', ...record.computedMeasures, ...props }); From ce76213abd441348ea2c546520018363f2472d31 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 12 Nov 2025 10:02:27 -0500 Subject: [PATCH 10/86] feat: collect username with user api call --- .../src/hooks/useInstrumentVisualization.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 98d0aa114..b4a99bb60 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -4,6 +4,7 @@ import { toBasicISOString } from '@douglasneuroinformatics/libjs'; import { useDownload, useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { AnyUnilingualScalarInstrument, InstrumentKind } from '@opendatacapture/runtime-core'; import type { Session } from '@opendatacapture/schemas/session'; +import type { User } from '@opendatacapture/schemas/user'; import { removeSubjectIdScope } from '@opendatacapture/subject-utils'; import axios from 'axios'; import { omit } from 'lodash-es'; @@ -56,7 +57,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); - const userInfo = async (sessionId: string): Promise => { + const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${sessionId}`); return response.data ? (response.data as Session) : null; @@ -66,6 +67,16 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }; + const userInfo = async (userId: string): Promise => { + try { + const response = await axios.get(`/v1/users/${userId}`); + return response.data ? (response.data as User) : null; + } catch (error) { + console.error('Error fetching user:', error); + return null; // ensures a resolved value instead of `void` + } + }; + const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); @@ -214,22 +225,25 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio for (const record of recordsQuery.data) { const props = record.data && typeof record.data === 'object' ? record.data : {}; - const userData = await userInfo(record.sessionId); - if (userData?.userId) { - // safely check since userData can be null + const sessionData = await sessionInfo(record.sessionId); + + if (!sessionData?.userId) { records.push({ __date__: record.date, __time__: record.date.getTime(), - userId: userData.userId, + userId: 'N/A', ...record.computedMeasures, ...props }); continue; } + + const userData = await userInfo(sessionData.userId); + // safely check since userData can be null records.push({ __date__: record.date, __time__: record.date.getTime(), - userId: 'N/A', + userId: userData?.username ?? 'N/A', ...record.computedMeasures, ...props }); From 1241f725d957c5af99a09950fef016f053f2518c Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 12 Nov 2025 16:28:58 -0500 Subject: [PATCH 11/86] refactor: move session and user api methods to separate hook files --- apps/web/src/hooks/useFindSession.ts | 12 +++++++++ apps/web/src/hooks/useFindUser.ts | 12 +++++++++ .../src/hooks/useInstrumentVisualization.ts | 26 +++---------------- 3 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 apps/web/src/hooks/useFindSession.ts create mode 100644 apps/web/src/hooks/useFindUser.ts diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts new file mode 100644 index 000000000..e3ae1592c --- /dev/null +++ b/apps/web/src/hooks/useFindSession.ts @@ -0,0 +1,12 @@ +import type { Session } from '@opendatacapture/schemas/session'; +import axios from 'axios'; + +export const sessionInfo = async (sessionId: string): Promise => { + try { + const response = await axios.get(`/v1/sessions/${sessionId}`); + return response.data ? (response.data as Session) : null; + } catch (error) { + console.error('Error fetching user:', error); + return null; // ensures a resolved value instead of `void` + } +}; diff --git a/apps/web/src/hooks/useFindUser.ts b/apps/web/src/hooks/useFindUser.ts new file mode 100644 index 000000000..a3c13dcab --- /dev/null +++ b/apps/web/src/hooks/useFindUser.ts @@ -0,0 +1,12 @@ +import type { User } from '@opendatacapture/schemas/user'; +import axios from 'axios'; + +export const userInfo = async (userId: string): Promise => { + try { + const response = await axios.get(`/v1/users/${userId}`); + return response.data ? (response.data as User) : null; + } catch (error) { + console.error('Error fetching user:', error); + return null; // ensures a resolved value instead of `void` + } +}; diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index b4a99bb60..635430ddb 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -3,10 +3,7 @@ import { useEffect, useMemo, useState } from 'react'; import { toBasicISOString } from '@douglasneuroinformatics/libjs'; import { useDownload, useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { AnyUnilingualScalarInstrument, InstrumentKind } from '@opendatacapture/runtime-core'; -import type { Session } from '@opendatacapture/schemas/session'; -import type { User } from '@opendatacapture/schemas/user'; import { removeSubjectIdScope } from '@opendatacapture/subject-utils'; -import axios from 'axios'; import { omit } from 'lodash-es'; import { unparse } from 'papaparse'; @@ -16,6 +13,9 @@ import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; +import { sessionInfo } from './useFindSession'; +import { userInfo } from './useFindUser'; + type InstrumentVisualizationRecord = { [key: string]: unknown; __date__: Date; @@ -57,26 +57,6 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); - const sessionInfo = async (sessionId: string): Promise => { - try { - const response = await axios.get(`/v1/sessions/${sessionId}`); - return response.data ? (response.data as Session) : null; - } catch (error) { - console.error('Error fetching user:', error); - return null; // ensures a resolved value instead of `void` - } - }; - - const userInfo = async (userId: string): Promise => { - try { - const response = await axios.get(`/v1/users/${userId}`); - return response.data ? (response.data as User) : null; - } catch (error) { - console.error('Error fetching user:', error); - return null; // ensures a resolved value instead of `void` - } - }; - const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); From 4c2b61442437aa01c96bc14306235eba703bd91d Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 12 Nov 2025 16:29:27 -0500 Subject: [PATCH 12/86] feat: create mocks for findUser and findSession hooks --- .../__tests__/useInstrumentVisualization.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index bfa1e7478..d1be55b24 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -37,6 +37,14 @@ const mockInstrumentRecords = { ] }; +const mockSession = { + sessionId: 123 +}; + +const mockUser = { + username: 'testusername' +}; + vi.mock('@/hooks/useInstrument', () => ({ useInstrument: mockUseInstrument })); @@ -63,6 +71,14 @@ vi.mock('@/hooks/useInstrumentRecords', () => ({ useInstrumentRecords: () => mockInstrumentRecords })); +vi.mock('@/hooks/useFindSession', () => ({ + sessionInfo: () => mockSession +})); + +vi.mock('@/hooks/useFindUser', () => ({ + userInfo: () => mockUser +})); + describe('useInstrumentVisualization', () => { beforeEach(() => { vi.clearAllMocks(); From 8f46e509fe17d4b0a770cf51e7c45c29156a6c6f Mon Sep 17 00:00:00 2001 From: David Roper Date: Thu, 13 Nov 2025 11:52:13 -0500 Subject: [PATCH 13/86] test: fix use tests with wait for methods --- .../useInstrumentVisualization.test.ts | 82 +++++++++++++------ 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index d1be55b24..4a4829cd4 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -1,5 +1,5 @@ import { toBasicISOString } from '@douglasneuroinformatics/libjs'; -import { act, renderHook } from '@testing-library/react'; +import { act, renderHook, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useInstrumentVisualization } from '../useInstrumentVisualization'; @@ -32,13 +32,14 @@ const mockInstrumentRecords = { { computedMeasures: {}, data: { someValue: 'abc' }, - date: FIXED_TEST_DATE + date: FIXED_TEST_DATE, + sessionId: '123' } ] }; const mockSession = { - sessionId: 123 + userId: '111' }; const mockUser = { @@ -85,9 +86,12 @@ describe('useInstrumentVisualization', () => { }); describe('CSV', () => { - it('Should download', () => { + it('Should download', async () => { const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); const { records } = result.current; + await waitFor(() => { + expect(result.current.records.length).toBeGreaterThan(0); + }); act(() => result.current.dl('CSV')); expect(records).toBeDefined(); expect(mockDownloadFn).toHaveBeenCalledTimes(1); @@ -95,30 +99,36 @@ describe('useInstrumentVisualization', () => { expect(filename).toContain('.csv'); const csvContents = getContentFn(); expect(csvContents).toMatch( - `GroupID,subjectId,Date,someValue\r\ntestGroupId,testId,${toBasicISOString(FIXED_TEST_DATE)},abc` + `GroupID,subjectId,Date,userId,someValue\r\ntestGroupId,testId,${toBasicISOString(FIXED_TEST_DATE)},testusername,abc` ); }); }); describe('TSV', () => { - it('Should download', () => { + it('Should download', async () => { const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); - const { dl, records } = result.current; - act(() => dl('TSV')); + const { records } = result.current; + await waitFor(() => { + expect(result.current.records.length).toBeGreaterThan(0); + }); + act(() => result.current.dl('TSV')); expect(records).toBeDefined(); expect(mockDownloadFn).toHaveBeenCalledTimes(1); const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? []; expect(filename).toContain('.tsv'); const tsvContents = getContentFn(); expect(tsvContents).toMatch( - `GroupID\tsubjectId\tDate\tsomeValue\r\ntestGroupId\ttestId\t${toBasicISOString(FIXED_TEST_DATE)}\tabc` + `GroupID\tsubjectId\tDate\tuserId\tsomeValue\r\ntestGroupId\ttestId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestusername\tabc` ); }); }); describe('CSV Long', () => { - it('Should download', () => { + it('Should download', async () => { const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); - const { dl, records } = result.current; - act(() => dl('CSV Long')); + const { records } = result.current; + await waitFor(() => { + expect(result.current.records.length).toBeGreaterThan(0); + }); + act(() => result.current.dl('CSV Long')); expect(records).toBeDefined(); expect(mockDownloadFn).toHaveBeenCalledTimes(1); @@ -126,15 +136,18 @@ describe('useInstrumentVisualization', () => { expect(filename).toContain('.csv'); const csvLongContents = getContentFn(); expect(csvLongContents).toMatch( - `GroupID,Date,SubjectID,Value,Variable\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testId,abc,someValue` + `GroupID,Date,SubjectID,Value,Variable\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testId,testusername,userId\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testId,abc,someValue` ); }); }); describe('TSV Long', () => { - it('Should download', () => { + it('Should download', async () => { const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); - const { dl, records } = result.current; - act(() => dl('TSV Long')); + const { records } = result.current; + await waitFor(() => { + expect(result.current.records.length).toBeGreaterThan(0); + }); + act(() => result.current.dl('TSV Long')); expect(records).toBeDefined(); expect(mockDownloadFn).toHaveBeenCalledTimes(1); @@ -142,15 +155,18 @@ describe('useInstrumentVisualization', () => { expect(filename).toMatch('.tsv'); const tsvLongContents = getContentFn(); expect(tsvLongContents).toMatch( - `GroupID\tDate\tSubjectID\tValue\tVariable\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestId\tabc\tsomeValue` + `GroupID\tDate\tSubjectID\tValue\tVariable\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestId\ttestusername\tuserId\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestId\tabc\tsomeValue` ); }); }); describe('Excel', () => { - it('Should download', () => { + it('Should download', async () => { const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); - const { dl, records } = result.current; - act(() => dl('Excel')); + const { records } = result.current; + await waitFor(() => { + expect(result.current.records.length).toBeGreaterThan(0); + }); + act(() => result.current.dl('Excel')); expect(records).toBeDefined(); expect(mockExcelDownloadFn).toHaveBeenCalledTimes(1); const [filename, getContentFn] = mockExcelDownloadFn.mock.calls[0] ?? []; @@ -163,16 +179,20 @@ describe('useInstrumentVisualization', () => { subjectId: 'testId', // eslint-disable-next-line perfectionist/sort-objects Date: '2025-04-30', - someValue: 'abc' + someValue: 'abc', + userId: 'testusername' } ]); }); }); describe('Excel Long', () => { - it('Should download', () => { + it('Should download', async () => { const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); - const { dl, records } = result.current; - act(() => dl('Excel Long')); + const { records } = result.current; + await waitFor(() => { + expect(result.current.records.length).toBeGreaterThan(0); + }); + act(() => result.current.dl('Excel Long')); expect(records).toBeDefined(); expect(mockExcelDownloadFn).toHaveBeenCalledTimes(1); @@ -181,6 +201,13 @@ describe('useInstrumentVisualization', () => { const excelContents = getContentFn; expect(excelContents).toEqual([ + { + Date: '2025-04-30', + GroupID: 'testGroupId', + SubjectID: 'testId', + Value: 'testusername', + Variable: 'userId' + }, { Date: '2025-04-30', GroupID: 'testGroupId', @@ -194,8 +221,11 @@ describe('useInstrumentVisualization', () => { describe('JSON', () => { it('Should download', async () => { const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); - const { dl, records } = result.current; - act(() => dl('JSON')); + const { records } = result.current; + await waitFor(() => { + expect(result.current.records.length).toBeGreaterThan(0); + }); + act(() => result.current.dl('JSON')); expect(records).toBeDefined(); expect(mockDownloadFn).toHaveBeenCalledTimes(1); From f9828f984be708f3ed3c30ac9b974c499aed4c2e Mon Sep 17 00:00:00 2001 From: david-roper Date: Fri, 14 Nov 2025 11:27:23 -0500 Subject: [PATCH 14/86] refactor: make Username a standalone column in long export formats --- apps/web/src/hooks/useInstrumentVisualization.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 635430ddb..2ba3c0651 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -96,12 +96,17 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio exportRecords.forEach((item) => { let date: Date; + let username: string; Object.entries(item).forEach(([objKey, objVal]) => { if (objKey === '__date__') { date = objVal as Date; return; } + if (objKey === 'userId') { + username = objVal as string; + return; + } if (Array.isArray(objVal)) { objVal.forEach((arrayItem) => { @@ -110,6 +115,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio GroupID: currentGroup ? currentGroup.id : 'root', // eslint-disable-next-line perfectionist/sort-objects Date: toBasicISOString(date), + Username: username, SubjectID: removeSubjectIdScope(params.subjectId), Variable: `${objKey}-${arrKey}`, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects @@ -122,6 +128,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio GroupID: currentGroup ? currentGroup.id : 'root', // eslint-disable-next-line perfectionist/sort-objects Date: toBasicISOString(date), + Username: username, SubjectID: removeSubjectIdScope(params.subjectId), Value: objVal, Variable: objKey From b8e362a2aee2bf373b2a1ed6e69e94464e7f8659 Mon Sep 17 00:00:00 2001 From: david-roper Date: Fri, 14 Nov 2025 11:27:47 -0500 Subject: [PATCH 15/86] test: change tests to include username --- .../__tests__/useInstrumentVisualization.test.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index 4a4829cd4..5f4920574 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -136,7 +136,7 @@ describe('useInstrumentVisualization', () => { expect(filename).toContain('.csv'); const csvLongContents = getContentFn(); expect(csvLongContents).toMatch( - `GroupID,Date,SubjectID,Value,Variable\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testId,testusername,userId\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testId,abc,someValue` + `GroupID,Date,Username,SubjectID,Value,Variable\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testusername,testId,abc,someValue` ); }); }); @@ -155,7 +155,7 @@ describe('useInstrumentVisualization', () => { expect(filename).toMatch('.tsv'); const tsvLongContents = getContentFn(); expect(tsvLongContents).toMatch( - `GroupID\tDate\tSubjectID\tValue\tVariable\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestId\ttestusername\tuserId\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestId\tabc\tsomeValue` + `GroupID\tDate\tUsername\tSubjectID\tValue\tVariable\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestusername\ttestId\tabc\tsomeValue` ); }); }); @@ -205,13 +205,7 @@ describe('useInstrumentVisualization', () => { Date: '2025-04-30', GroupID: 'testGroupId', SubjectID: 'testId', - Value: 'testusername', - Variable: 'userId' - }, - { - Date: '2025-04-30', - GroupID: 'testGroupId', - SubjectID: 'testId', + Username: 'testusername', Value: 'abc', Variable: 'someValue' } From ee04c0482828b581f0c464830613aa05f4f479e9 Mon Sep 17 00:00:00 2001 From: david-roper Date: Fri, 14 Nov 2025 15:07:47 -0500 Subject: [PATCH 16/86] chore: small changes to test From 279da7c364db924866d076511f197bd182379839 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 11:37:01 -0500 Subject: [PATCH 17/86] fix: fix description in session controller --- apps/api/src/sessions/sessions.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 34d9f40ed..437cff75b 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -27,7 +27,7 @@ export class SessionsController { return this.sessionsService.findById(id, { ability }); } - @ApiOperation({ description: 'Find Session by ID' }) + @ApiOperation({ description: 'Find Sessions by ID' }) @Post('list') @RouteAccess({ action: 'read', subject: 'Session' }) findSessionList(@Query('ids') ids: string[]): Promise { From f46ddd2c51b1f6de649735d2c29755dea2f992c3 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 11:38:52 -0500 Subject: [PATCH 18/86] fix: add ability to find sessions list --- apps/api/src/sessions/sessions.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 437cff75b..286d340c1 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -30,8 +30,8 @@ export class SessionsController { @ApiOperation({ description: 'Find Sessions by ID' }) @Post('list') @RouteAccess({ action: 'read', subject: 'Session' }) - findSessionList(@Query('ids') ids: string[]): Promise { + findSessionList(@Query('ids') ids: string[], @CurrentUser('ability') ability: AppAbility): Promise { const idArray = Array.isArray(ids) ? ids : (ids as string).split(','); - return this.sessionsService.findSessionList(idArray); + return this.sessionsService.findSessionList(idArray, { ability }); } } From 6cc7066fa8d5d94ca8401b3a076e316ddefd5250 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 11:58:21 -0500 Subject: [PATCH 19/86] fix: error msg in usefindsession --- apps/web/src/hooks/useFindSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index e3ae1592c..08d3ad54e 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -6,7 +6,7 @@ export const sessionInfo = async (sessionId: string): Promise => const response = await axios.get(`/v1/sessions/${sessionId}`); return response.data ? (response.data as Session) : null; } catch (error) { - console.error('Error fetching user:', error); + console.error('Error fetching session:', error); return null; // ensures a resolved value instead of `void` } }; From 2e72fde9a62e2fc466b4607560f5a80acba537fe Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 14:42:17 -0500 Subject: [PATCH 20/86] fix: throw and error to catch instead of returning null when error occurs --- apps/web/src/hooks/useFindSession.ts | 2 +- apps/web/src/hooks/useFindUser.ts | 2 +- .../src/hooks/useInstrumentVisualization.ts | 46 ++++++++++--------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 08d3ad54e..4232834bb 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -7,6 +7,6 @@ export const sessionInfo = async (sessionId: string): Promise => return response.data ? (response.data as Session) : null; } catch (error) { console.error('Error fetching session:', error); - return null; // ensures a resolved value instead of `void` + throw error; } }; diff --git a/apps/web/src/hooks/useFindUser.ts b/apps/web/src/hooks/useFindUser.ts index a3c13dcab..686efd1c9 100644 --- a/apps/web/src/hooks/useFindUser.ts +++ b/apps/web/src/hooks/useFindUser.ts @@ -7,6 +7,6 @@ export const userInfo = async (userId: string): Promise => { return response.data ? (response.data as User) : null; } catch (error) { console.error('Error fetching user:', error); - return null; // ensures a resolved value instead of `void` + throw error; } }; diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 2ba3c0651..e015062f9 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -206,37 +206,41 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio useEffect(() => { const fetchRecords = async () => { - if (recordsQuery.data) { - const records: InstrumentVisualizationRecord[] = []; - - for (const record of recordsQuery.data) { - const props = record.data && typeof record.data === 'object' ? record.data : {}; - - const sessionData = await sessionInfo(record.sessionId); + try { + if (recordsQuery.data) { + const records: InstrumentVisualizationRecord[] = []; + + for (const record of recordsQuery.data) { + const props = record.data && typeof record.data === 'object' ? record.data : {}; + + const sessionData = await sessionInfo(record.sessionId); + + if (!sessionData?.userId) { + records.push({ + __date__: record.date, + __time__: record.date.getTime(), + userId: 'N/A', + ...record.computedMeasures, + ...props + }); + continue; + } - if (!sessionData?.userId) { + const userData = await userInfo(sessionData.userId); + // safely check since userData can be null records.push({ __date__: record.date, __time__: record.date.getTime(), - userId: 'N/A', + userId: userData?.username ?? 'N/A', ...record.computedMeasures, ...props }); - continue; } - const userData = await userInfo(sessionData.userId); - // safely check since userData can be null - records.push({ - __date__: record.date, - __time__: record.date.getTime(), - userId: userData?.username ?? 'N/A', - ...record.computedMeasures, - ...props - }); + setRecords(records); } - - setRecords(records); + } catch (error) { + console.error('Error occurred: ', error); } }; void fetchRecords(); From 8d545ad58426ed6134e4ca449b8e6375a05bddaa Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 14:58:26 -0500 Subject: [PATCH 21/86] feat: use schema parsing to confirm contents instead of casting it --- apps/web/src/hooks/useFindSession.ts | 5 +++-- apps/web/src/hooks/useFindUser.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 4232834bb..1360a5b66 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,10 +1,11 @@ -import type { Session } from '@opendatacapture/schemas/session'; +import { type Session, $Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${sessionId}`); - return response.data ? (response.data as Session) : null; + const parsedResult = $Session.safeParse(response.data); + return parsedResult.success ? parsedResult.data : null; } catch (error) { console.error('Error fetching session:', error); throw error; diff --git a/apps/web/src/hooks/useFindUser.ts b/apps/web/src/hooks/useFindUser.ts index 686efd1c9..b65d53bff 100644 --- a/apps/web/src/hooks/useFindUser.ts +++ b/apps/web/src/hooks/useFindUser.ts @@ -1,10 +1,11 @@ -import type { User } from '@opendatacapture/schemas/user'; +import { type User, $User } from '@opendatacapture/schemas/user'; import axios from 'axios'; export const userInfo = async (userId: string): Promise => { try { const response = await axios.get(`/v1/users/${userId}`); - return response.data ? (response.data as User) : null; + const parsedResult = $User.safeParse(response.data); + return parsedResult.success ? parsedResult.data : null; } catch (error) { console.error('Error fetching user:', error); throw error; From 307effc7a9abc83228fe6368025722e32fd5e33a Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 15:47:51 -0500 Subject: [PATCH 22/86] feat: adjust username variable to start as N/A, adjust tests --- .../hooks/__tests__/useInstrumentVisualization.test.ts | 7 +++---- apps/web/src/hooks/useInstrumentVisualization.ts | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index 5f4920574..7b43e4b73 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -99,7 +99,7 @@ describe('useInstrumentVisualization', () => { expect(filename).toContain('.csv'); const csvContents = getContentFn(); expect(csvContents).toMatch( - `GroupID,subjectId,Date,userId,someValue\r\ntestGroupId,testId,${toBasicISOString(FIXED_TEST_DATE)},testusername,abc` + `GroupID,subjectId,Date,username,someValue\r\ntestGroupId,testId,${toBasicISOString(FIXED_TEST_DATE)},testusername,abc` ); }); }); @@ -117,7 +117,7 @@ describe('useInstrumentVisualization', () => { expect(filename).toContain('.tsv'); const tsvContents = getContentFn(); expect(tsvContents).toMatch( - `GroupID\tsubjectId\tDate\tuserId\tsomeValue\r\ntestGroupId\ttestId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestusername\tabc` + `GroupID\tsubjectId\tDate\tusername\tsomeValue\r\ntestGroupId\ttestId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestusername\tabc` ); }); }); @@ -177,10 +177,9 @@ describe('useInstrumentVisualization', () => { { GroupID: 'testGroupId', subjectId: 'testId', - // eslint-disable-next-line perfectionist/sort-objects Date: '2025-04-30', someValue: 'abc', - userId: 'testusername' + username: 'testusername' } ]); }); diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index e015062f9..862275022 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -96,14 +96,14 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio exportRecords.forEach((item) => { let date: Date; - let username: string; + let username: string = 'N/A'; Object.entries(item).forEach(([objKey, objVal]) => { if (objKey === '__date__') { date = objVal as Date; return; } - if (objKey === 'userId') { + if (objKey === 'username') { username = objVal as string; return; } @@ -219,7 +219,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio records.push({ __date__: record.date, __time__: record.date.getTime(), - userId: 'N/A', + username: 'N/A', ...record.computedMeasures, ...props }); @@ -231,7 +231,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio records.push({ __date__: record.date, __time__: record.date.getTime(), - userId: userData?.username ?? 'N/A', + username: userData?.username ?? 'N/A', ...record.computedMeasures, ...props }); From 527543c104eaf10b15d5fc2927bc3b5f2343a538 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 15:49:03 -0500 Subject: [PATCH 23/86] fix: fix type exports --- apps/web/src/hooks/useFindSession.ts | 3 ++- apps/web/src/hooks/useFindUser.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 1360a5b66..2c838cf2a 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,4 +1,5 @@ -import { type Session, $Session } from '@opendatacapture/schemas/session'; +import { $Session } from '@opendatacapture/schemas/session'; +import type { Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { diff --git a/apps/web/src/hooks/useFindUser.ts b/apps/web/src/hooks/useFindUser.ts index b65d53bff..9dea9202f 100644 --- a/apps/web/src/hooks/useFindUser.ts +++ b/apps/web/src/hooks/useFindUser.ts @@ -1,4 +1,5 @@ -import { type User, $User } from '@opendatacapture/schemas/user'; +import { $User } from '@opendatacapture/schemas/user'; +import type { User } from '@opendatacapture/schemas/user'; import axios from 'axios'; export const userInfo = async (userId: string): Promise => { From 8ed39228e42e3364320c499ed280dca17c1eed32 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 16:09:25 -0500 Subject: [PATCH 24/86] test: change positions of subjectId and username column to make linter happy with itself --- .../src/hooks/__tests__/useInstrumentVisualization.test.ts | 6 +++--- apps/web/src/hooks/useInstrumentVisualization.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index 7b43e4b73..88cb1afc4 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -136,7 +136,7 @@ describe('useInstrumentVisualization', () => { expect(filename).toContain('.csv'); const csvLongContents = getContentFn(); expect(csvLongContents).toMatch( - `GroupID,Date,Username,SubjectID,Value,Variable\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testusername,testId,abc,someValue` + `GroupID,Date,SubjectID,Username,Value,Variable\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testId,testusername,abc,someValue` ); }); }); @@ -155,7 +155,7 @@ describe('useInstrumentVisualization', () => { expect(filename).toMatch('.tsv'); const tsvLongContents = getContentFn(); expect(tsvLongContents).toMatch( - `GroupID\tDate\tUsername\tSubjectID\tValue\tVariable\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestusername\ttestId\tabc\tsomeValue` + `GroupID\tDate\tSubjectID\tUsername\tValue\tVariable\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestId\ttestusername\tabc\tsomeValue` ); }); }); @@ -175,9 +175,9 @@ describe('useInstrumentVisualization', () => { expect(excelContents).toEqual([ { + Date: '2025-04-30', GroupID: 'testGroupId', subjectId: 'testId', - Date: '2025-04-30', someValue: 'abc', username: 'testusername' } diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 862275022..6eb6c3ce9 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -96,7 +96,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio exportRecords.forEach((item) => { let date: Date; - let username: string = 'N/A'; + let username = 'N/A'; Object.entries(item).forEach(([objKey, objVal]) => { if (objKey === '__date__') { @@ -115,8 +115,8 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio GroupID: currentGroup ? currentGroup.id : 'root', // eslint-disable-next-line perfectionist/sort-objects Date: toBasicISOString(date), - Username: username, SubjectID: removeSubjectIdScope(params.subjectId), + Username: username, Variable: `${objKey}-${arrKey}`, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects Value: arrItem @@ -128,8 +128,8 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio GroupID: currentGroup ? currentGroup.id : 'root', // eslint-disable-next-line perfectionist/sort-objects Date: toBasicISOString(date), - Username: username, SubjectID: removeSubjectIdScope(params.subjectId), + Username: username, Value: objVal, Variable: objKey }); From f88971dda80512e15281c5dfefaade207d90e9bf Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 16:09:50 -0500 Subject: [PATCH 25/86] refactor: remove unused api call --- apps/api/src/sessions/sessions.controller.ts | 8 -------- apps/api/src/sessions/sessions.service.ts | 15 --------------- 2 files changed, 23 deletions(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 286d340c1..132299ad2 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -26,12 +26,4 @@ export class SessionsController { findByID(@Param('id') id: string, @CurrentUser('ability') ability: AppAbility): Promise { return this.sessionsService.findById(id, { ability }); } - - @ApiOperation({ description: 'Find Sessions by ID' }) - @Post('list') - @RouteAccess({ action: 'read', subject: 'Session' }) - findSessionList(@Query('ids') ids: string[], @CurrentUser('ability') ability: AppAbility): Promise { - const idArray = Array.isArray(ids) ? ids : (ids as string).split(','); - return this.sessionsService.findSessionList(idArray, { ability }); - } } diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index ee84a55a7..3d01b0fe1 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -104,21 +104,6 @@ export class SessionsService { return session; } - async findSessionList(ids: string[], { ability }: EntityOperationOptions = {}) { - const sessionsArray = await Promise.all( - ids.map(async (id) => { - const session = await this.sessionModel.findFirst({ - where: { AND: [accessibleQuery(ability, 'read', 'Session')], id } - }); - if (!session) { - throw new NotFoundException(`Failed to find session with ID: ${id}`); - } - return session; - }) - ); - return sessionsArray; - } - /** Get the subject if they exist, otherwise create them */ private async resolveSubject(subjectData: CreateSubjectData) { this.loggingService.debug({ message: 'Attempting to resolve subject', subjectData }); From f53062a1ab7f377b050960ea17f2029a521436f2 Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 09:44:08 -0500 Subject: [PATCH 26/86] chore: linter fixes --- apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index 88cb1afc4..70430d756 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -178,6 +178,7 @@ describe('useInstrumentVisualization', () => { Date: '2025-04-30', GroupID: 'testGroupId', subjectId: 'testId', + // eslint-disable-next-line perfectionist/sort-objects someValue: 'abc', username: 'testusername' } From 6d1b7103541337e91823e558b4aa83e0a97cef9d Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 09:53:19 -0500 Subject: [PATCH 27/86] test: add resolved promise is session and userinfo mocked methods --- .../src/hooks/__tests__/useInstrumentVisualization.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index 70430d756..51db05b5c 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -73,11 +73,11 @@ vi.mock('@/hooks/useInstrumentRecords', () => ({ })); vi.mock('@/hooks/useFindSession', () => ({ - sessionInfo: () => mockSession + sessionInfo: () => Promise.resolve(mockSession) })); vi.mock('@/hooks/useFindUser', () => ({ - userInfo: () => mockUser + userInfo: () => Promise.resolve(mockUser) })); describe('useInstrumentVisualization', () => { From fa97ac90b5e8108f4e8859673e761d7ced97c297 Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 11:18:31 -0500 Subject: [PATCH 28/86] fix: remove extra append statement in excel download method --- apps/web/src/utils/excel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/utils/excel.ts b/apps/web/src/utils/excel.ts index 74fa3a92f..82cec9d44 100644 --- a/apps/web/src/utils/excel.ts +++ b/apps/web/src/utils/excel.ts @@ -16,6 +16,5 @@ export function downloadSubjectTableExcel(filename: string, records: { [key: str .trim() || 'Subject'; // Fallback if empty const workbook = utils.book_new(); utils.book_append_sheet(workbook, utils.json_to_sheet(records), sanitizedName); - utils.book_append_sheet(workbook, utils.json_to_sheet(records), name); writeFileXLSX(workbook, filename); } From 51e121a1cb9af03da3c725f37e34d913e84cd07b Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 14:35:32 -0500 Subject: [PATCH 29/86] feat: fix not finding user id issue by making subject inclusion optional --- apps/web/src/hooks/useFindSession.ts | 4 ++-- packages/schemas/src/session/session.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 2c838cf2a..ffc1fb7fe 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -5,8 +5,8 @@ import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${sessionId}`); - const parsedResult = $Session.safeParse(response.data); - return parsedResult.success ? parsedResult.data : null; + const parsedResult = await $Session.parseAsync(response.data); + return parsedResult; } catch (error) { console.error('Error fetching session:', error); throw error; diff --git a/packages/schemas/src/session/session.ts b/packages/schemas/src/session/session.ts index 81d38bead..6b5d982fc 100644 --- a/packages/schemas/src/session/session.ts +++ b/packages/schemas/src/session/session.ts @@ -10,7 +10,7 @@ export type Session = z.infer; export const $Session = $BaseModel.extend({ date: z.coerce.date(), groupId: z.string().nullable(), - subject: $Subject, + subject: $Subject.optional(), subjectId: z.string(), type: $SessionType, userId: z.string().nullish() From 53016ce53ce53f49bd435275f9ee0824b804326c Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 14:44:59 -0500 Subject: [PATCH 30/86] chore: revert session schema and parse change --- apps/web/src/hooks/useFindSession.ts | 7 ++++--- packages/schemas/src/session/session.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index ffc1fb7fe..01fb77508 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,12 +1,13 @@ -import { $Session } from '@opendatacapture/schemas/session'; import type { Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${sessionId}`); - const parsedResult = await $Session.parseAsync(response.data); - return parsedResult; + if (!response.data) { + throw new Error('Session data does not exist'); + } + return response.data as Session; } catch (error) { console.error('Error fetching session:', error); throw error; diff --git a/packages/schemas/src/session/session.ts b/packages/schemas/src/session/session.ts index 6b5d982fc..81d38bead 100644 --- a/packages/schemas/src/session/session.ts +++ b/packages/schemas/src/session/session.ts @@ -10,7 +10,7 @@ export type Session = z.infer; export const $Session = $BaseModel.extend({ date: z.coerce.date(), groupId: z.string().nullable(), - subject: $Subject.optional(), + subject: $Subject, subjectId: z.string(), type: $SessionType, userId: z.string().nullish() From 576794eb0b997aa39baef281b9639a7d69f3f2d7 Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 14:46:22 -0500 Subject: [PATCH 31/86] fix: make username column in wideRow method and its tests more consistent --- .../src/hooks/__tests__/useInstrumentVisualization.test.ts | 6 +++--- apps/web/src/hooks/useInstrumentVisualization.ts | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index 51db05b5c..ded589273 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -99,7 +99,7 @@ describe('useInstrumentVisualization', () => { expect(filename).toContain('.csv'); const csvContents = getContentFn(); expect(csvContents).toMatch( - `GroupID,subjectId,Date,username,someValue\r\ntestGroupId,testId,${toBasicISOString(FIXED_TEST_DATE)},testusername,abc` + `GroupID,subjectId,Date,Username,someValue\r\ntestGroupId,testId,${toBasicISOString(FIXED_TEST_DATE)},testusername,abc` ); }); }); @@ -117,7 +117,7 @@ describe('useInstrumentVisualization', () => { expect(filename).toContain('.tsv'); const tsvContents = getContentFn(); expect(tsvContents).toMatch( - `GroupID\tsubjectId\tDate\tusername\tsomeValue\r\ntestGroupId\ttestId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestusername\tabc` + `GroupID\tsubjectId\tDate\tUsername\tsomeValue\r\ntestGroupId\ttestId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestusername\tabc` ); }); }); @@ -180,7 +180,7 @@ describe('useInstrumentVisualization', () => { subjectId: 'testId', // eslint-disable-next-line perfectionist/sort-objects someValue: 'abc', - username: 'testusername' + Username: 'testusername' } ]); }); diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 6eb6c3ce9..2e180d5d6 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -85,6 +85,10 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio obj.Date = toBasicISOString(val as Date); continue; } + if (key === 'username') { + obj.Username = val; + continue; + } obj[key] = typeof val === 'object' ? JSON.stringify(val) : val; } return obj; From ebed1a30abddacc0430ec362f0a0098f985a71c0 Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 15:06:24 -0500 Subject: [PATCH 32/86] feat: remove redundant null return type from useFindSession --- apps/web/src/hooks/useFindSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 01fb77508..676c908fc 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,7 +1,7 @@ import type { Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; -export const sessionInfo = async (sessionId: string): Promise => { +export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${sessionId}`); if (!response.data) { From 3dc1a8529d68d9f353d29192a35a52074fa8a54b Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 15:10:01 -0500 Subject: [PATCH 33/86] feat: add error notification for useEffect --- apps/web/src/hooks/useInstrumentVisualization.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 2e180d5d6..23ec89617 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -245,6 +245,13 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } } catch (error) { console.error('Error occurred: ', error); + notifications.addNotification({ + message: t({ + en: 'Error occurred finding records', + fr: "Une erreur s'est produite lors de la recherche des enregistrements." + }), + type: 'error' + }); } }; void fetchRecords(); From ac7316f0b9b3e3bb7c6ad5cdbefa72f9d6dbafce Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 19 Nov 2025 10:16:35 -0500 Subject: [PATCH 34/86] chore: and encodeUriComponent to ids --- apps/web/src/hooks/useFindSession.ts | 2 +- apps/web/src/hooks/useFindUser.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 676c908fc..333890c62 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -3,7 +3,7 @@ import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { try { - const response = await axios.get(`/v1/sessions/${sessionId}`); + const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); if (!response.data) { throw new Error('Session data does not exist'); } diff --git a/apps/web/src/hooks/useFindUser.ts b/apps/web/src/hooks/useFindUser.ts index 9dea9202f..6dc2a95d1 100644 --- a/apps/web/src/hooks/useFindUser.ts +++ b/apps/web/src/hooks/useFindUser.ts @@ -4,7 +4,7 @@ import axios from 'axios'; export const userInfo = async (userId: string): Promise => { try { - const response = await axios.get(`/v1/users/${userId}`); + const response = await axios.get(`/v1/users/${encodeURIComponent(userId)}`); const parsedResult = $User.safeParse(response.data); return parsedResult.success ? parsedResult.data : null; } catch (error) { From ea000e0bb03dcc47131dae069096c7bcf782fd94 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 19 Nov 2025 10:43:19 -0500 Subject: [PATCH 35/86] feat: fetch sessions then users in parrallel and update test mock values --- .../useInstrumentVisualization.test.ts | 1 + .../src/hooks/useInstrumentVisualization.ts | 37 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index ded589273..db12e8427 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -43,6 +43,7 @@ const mockSession = { }; const mockUser = { + id: '111', username: 'testusername' }; diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 23ec89617..f023e8e34 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -212,34 +212,31 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio const fetchRecords = async () => { try { if (recordsQuery.data) { - const records: InstrumentVisualizationRecord[] = []; + // Fetch all sessions in parallel + const sessionPromises = recordsQuery.data.map((record) => sessionInfo(record.sessionId)); + const sessions = await Promise.all(sessionPromises); - for (const record of recordsQuery.data) { - const props = record.data && typeof record.data === 'object' ? record.data : {}; + // Extract unique userIds and fetch users in parallel + const userIds = [...new Set(sessions.filter((s) => s?.userId).map((s) => s.userId))]; - const sessionData = await sessionInfo(record.sessionId); + const userPromises = userIds.map((userId) => userInfo(userId!)); + const users = await Promise.all(userPromises); + const userMap = new Map(users.filter((u) => u).map((u) => [u!.id, u!.username])); - if (!sessionData?.userId) { - records.push({ - __date__: record.date, - __time__: record.date.getTime(), - username: 'N/A', - ...record.computedMeasures, - ...props - }); - continue; - } + // Build records with looked-up data + const records: InstrumentVisualizationRecord[] = recordsQuery.data.map((record, i) => { + const props = record.data && typeof record.data === 'object' ? record.data : {}; + const session = sessions[i]; + const username = session?.userId ? (userMap.get(session.userId) ?? 'N/A') : 'N/A'; - const userData = await userInfo(sessionData.userId); - // safely check since userData can be null - records.push({ + return { __date__: record.date, __time__: record.date.getTime(), - username: userData?.username ?? 'N/A', + username: username, ...record.computedMeasures, ...props - }); - } + }; + }); setRecords(records); } From d27535aea114a50d9b0e66da9e0c49176844af88 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 19 Nov 2025 11:35:17 -0500 Subject: [PATCH 36/86] feat: add cancelled var to avoid race conditions in fetch records --- apps/web/src/hooks/useInstrumentVisualization.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index f023e8e34..42f1c566c 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -209,6 +209,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio }; useEffect(() => { + let cancelled = false; const fetchRecords = async () => { try { if (recordsQuery.data) { @@ -219,6 +220,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio // Extract unique userIds and fetch users in parallel const userIds = [...new Set(sessions.filter((s) => s?.userId).map((s) => s.userId))]; + //assume userId exists in userId set as we already filtered out the non-existing userIds const userPromises = userIds.map((userId) => userInfo(userId!)); const users = await Promise.all(userPromises); const userMap = new Map(users.filter((u) => u).map((u) => [u!.id, u!.username])); @@ -238,7 +240,9 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio }; }); - setRecords(records); + if (!cancelled) { + setRecords(records); + } } } catch (error) { console.error('Error occurred: ', error); @@ -252,6 +256,9 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }; void fetchRecords(); + return () => { + cancelled = true; + }; }, [recordsQuery.data]); const instrumentOptions: { [key: string]: string } = useMemo(() => { From 763be840a4add5c918725fd93a7b5e22c460ba47 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 19 Nov 2025 11:51:57 -0500 Subject: [PATCH 37/86] feat: return null on errors userInfo issues --- apps/web/src/hooks/useInstrumentVisualization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 42f1c566c..ff41b8776 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -221,7 +221,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio const userIds = [...new Set(sessions.filter((s) => s?.userId).map((s) => s.userId))]; //assume userId exists in userId set as we already filtered out the non-existing userIds - const userPromises = userIds.map((userId) => userInfo(userId!)); + const userPromises = userIds.map((userId) => userInfo(userId!).catch(() => null)); const users = await Promise.all(userPromises); const userMap = new Map(users.filter((u) => u).map((u) => [u!.id, u!.username])); From 089598e328056700ea81ade1dd8258402693d96a Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 19 Nov 2025 17:06:02 -0500 Subject: [PATCH 38/86] feat: add new findAllSessionsIncludeUsernames api call and todo comments --- apps/api/src/sessions/sessions.controller.ts | 10 ++++++++++ apps/api/src/sessions/sessions.service.ts | 15 +++++++++++++++ apps/web/src/hooks/useFindSession.ts | 6 ++++++ apps/web/src/hooks/useInstrumentVisualization.ts | 3 +++ packages/schemas/src/session/session.ts | 7 +++++++ 5 files changed, 41 insertions(+) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 132299ad2..8524fe131 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -20,6 +20,16 @@ export class SessionsController { return this.sessionsService.create(data); } + @ApiOperation({ description: 'Find all sessions and usernames attached to them' }) + @Get() + @RouteAccess({ action: 'read', subject: 'Session' }) + findAllIncludeUsernames( + @Query('groupId') groupId: string, + @CurrentUser('ability') ability: AppAbility + ) { + return this.sessionsService.findAllIncludeUsernames(groupId, { ability }); + } + @ApiOperation({ description: 'Find Session by ID' }) @Get(':id') @RouteAccess({ action: 'read', subject: 'Session' }) diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index 3d01b0fe1..451c6083c 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -94,6 +94,21 @@ export class SessionsService { }); } + async findAllIncludeUsernames(groupId: string, { ability }: EntityOperationOptions = {}) { + return this.sessionModel.findMany({ + include: { + user: { + select: { + username: true + } + } + }, + where: { + AND: [accessibleQuery(ability, 'read', 'Session'), { groupId }] + } + }); + } + async findById(id: string, { ability }: EntityOperationOptions = {}) { const session = await this.sessionModel.findFirst({ where: { AND: [accessibleQuery(ability, 'read', 'Session')], id } diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 333890c62..a3aac0c34 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,6 +1,12 @@ import type { Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; +//Change this query to into a hook method and name it useFindSessionQuery + +//Change the api call to have an include tag which includes the username from users + +//Change the return type to + export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index ff41b8776..48391b337 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -57,6 +57,9 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); + // Create a new sessionsUsernameQuery which uses the useFindSessionQuery hook + // have use a different return type with + const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); diff --git a/packages/schemas/src/session/session.ts b/packages/schemas/src/session/session.ts index 81d38bead..8b0a89b83 100644 --- a/packages/schemas/src/session/session.ts +++ b/packages/schemas/src/session/session.ts @@ -24,3 +24,10 @@ export const $CreateSessionData = z.object({ type: $SessionType, username: z.string().nullish() }); + +export type $SessionWithUser = z.infer; +export const $SessionWithUser = $Session.extend({ + user: z.object({ + username: z.string().nullish() + }) +}); From 05fbd7c7328441c1f32bc285e15fb321f6a85d39 Mon Sep 17 00:00:00 2001 From: david-roper Date: Thu, 20 Nov 2025 13:40:04 -0500 Subject: [PATCH 39/86] feat: rename function to useFindSessionQuery --- apps/web/src/hooks/useFindSession.ts | 21 ---------- apps/web/src/hooks/useFindSessionQuery.ts | 51 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 21 deletions(-) delete mode 100644 apps/web/src/hooks/useFindSession.ts create mode 100644 apps/web/src/hooks/useFindSessionQuery.ts diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts deleted file mode 100644 index a3aac0c34..000000000 --- a/apps/web/src/hooks/useFindSession.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Session } from '@opendatacapture/schemas/session'; -import axios from 'axios'; - -//Change this query to into a hook method and name it useFindSessionQuery - -//Change the api call to have an include tag which includes the username from users - -//Change the return type to - -export const sessionInfo = async (sessionId: string): Promise => { - try { - const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); - if (!response.data) { - throw new Error('Session data does not exist'); - } - return response.data as Session; - } catch (error) { - console.error('Error fetching session:', error); - throw error; - } -}; diff --git a/apps/web/src/hooks/useFindSessionQuery.ts b/apps/web/src/hooks/useFindSessionQuery.ts new file mode 100644 index 000000000..004912331 --- /dev/null +++ b/apps/web/src/hooks/useFindSessionQuery.ts @@ -0,0 +1,51 @@ +import { reviver } from '@douglasneuroinformatics/libjs'; +import { + $SessionWithUser, + type Session, + type SessionWithUser, + type SessionWithUserQueryParams +} from '@opendatacapture/schemas/session'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +//Change this query to into a hook method and name it useFindSessionQuery + +//Change the api call to have an include tag which includes the username from users + +//Change the return type to + +type UseSessionOptions = { + enabled?: boolean; + params: SessionWithUserQueryParams; +}; + +export const sessionInfo = async (sessionId: string): Promise => { + try { + const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); + if (!response.data) { + throw new Error('Session data does not exist'); + } + return response.data as Session; + } catch (error) { + console.error('Error fetching session:', error); + throw error; + } +}; + +export const useFindSessionQuery = ( + { enabled, params }: UseSessionOptions = { + enabled: true, + params: {} + } +) => { + return useQuery({ + enabled, + queryFn: async () => { + const response = await axios.get('/v1/sessions/', { + params + }); + return $SessionWithUser.array().parseAsync(response.data); + }, + queryKey: ['sessions', ...Object.values(params)] + }); +}; From f36631aecda0b9f82c44760be3d375606f58edfb Mon Sep 17 00:00:00 2001 From: david-roper Date: Thu, 20 Nov 2025 13:41:40 -0500 Subject: [PATCH 40/86] feat: update types of findAllIncludeUsernames --- apps/api/src/sessions/sessions.controller.ts | 7 ++++--- apps/api/src/sessions/sessions.service.ts | 8 ++++++-- apps/web/src/hooks/useInstrumentVisualization.ts | 9 ++++++++- packages/schemas/src/session/session.ts | 6 +++++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 8524fe131..0b9e50773 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -8,6 +8,7 @@ import { RouteAccess } from '@/core/decorators/route-access.decorator'; import { CreateSessionDto } from './dto/create-session.dto'; import { SessionsService } from './sessions.service'; +import type { SessionWithUser } from '@opendatacapture/schemas/session'; @Controller('sessions') export class SessionsController { @@ -24,9 +25,9 @@ export class SessionsController { @Get() @RouteAccess({ action: 'read', subject: 'Session' }) findAllIncludeUsernames( - @Query('groupId') groupId: string, - @CurrentUser('ability') ability: AppAbility - ) { + @CurrentUser('ability') ability: AppAbility, + @Query('groupId') groupId?: string + ): Promise { return this.sessionsService.findAllIncludeUsernames(groupId, { ability }); } diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index 451c6083c..8bffa3477 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -94,8 +94,8 @@ export class SessionsService { }); } - async findAllIncludeUsernames(groupId: string, { ability }: EntityOperationOptions = {}) { - return this.sessionModel.findMany({ + async findAllIncludeUsernames(groupId?: string, { ability }: EntityOperationOptions = {}) { + const sessionsWithUsers = await this.sessionModel.findMany({ include: { user: { select: { @@ -107,6 +107,10 @@ export class SessionsService { AND: [accessibleQuery(ability, 'read', 'Session'), { groupId }] } }); + if (!sessionsWithUsers) { + throw new NotFoundException(`Failed to find users`); + } + return sessionsWithUsers; } async findById(id: string, { ability }: EntityOperationOptions = {}) { diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 48391b337..6a98a4206 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -13,7 +13,7 @@ import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; -import { sessionInfo } from './useFindSession'; +import { sessionInfo, useFindSessionQuery } from './useFindSessionQuery'; import { userInfo } from './useFindUser'; type InstrumentVisualizationRecord = { @@ -57,6 +57,13 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); + // const sessionsUsernameQuery = useFindSessionQuery({ + // enabled: instrumentId !== null, + // params: { + // groupId: currentGroup?.id + // } + // }); + // Create a new sessionsUsernameQuery which uses the useFindSessionQuery hook // have use a different return type with diff --git a/packages/schemas/src/session/session.ts b/packages/schemas/src/session/session.ts index 8b0a89b83..20d2a83b8 100644 --- a/packages/schemas/src/session/session.ts +++ b/packages/schemas/src/session/session.ts @@ -25,9 +25,13 @@ export const $CreateSessionData = z.object({ username: z.string().nullish() }); -export type $SessionWithUser = z.infer; +export type SessionWithUser = z.infer; export const $SessionWithUser = $Session.extend({ user: z.object({ username: z.string().nullish() }) }); + +export type SessionWithUserQueryParams = { + groupId?: string; +}; From 1f6f093107681cf0b92b9073571bef6a94cedd3b Mon Sep 17 00:00:00 2001 From: david-roper Date: Thu, 20 Nov 2025 15:41:43 -0500 Subject: [PATCH 41/86] feat: update query and type imports --- apps/api/src/sessions/sessions.service.ts | 3 ++- apps/web/src/hooks/useFindSessionQuery.ts | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index 8bffa3477..14f4bf0f3 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -101,7 +101,8 @@ export class SessionsService { select: { username: true } - } + }, + subject: true }, where: { AND: [accessibleQuery(ability, 'read', 'Session'), { groupId }] diff --git a/apps/web/src/hooks/useFindSessionQuery.ts b/apps/web/src/hooks/useFindSessionQuery.ts index 004912331..f6b027d52 100644 --- a/apps/web/src/hooks/useFindSessionQuery.ts +++ b/apps/web/src/hooks/useFindSessionQuery.ts @@ -1,10 +1,5 @@ -import { reviver } from '@douglasneuroinformatics/libjs'; -import { - $SessionWithUser, - type Session, - type SessionWithUser, - type SessionWithUserQueryParams -} from '@opendatacapture/schemas/session'; +import { $SessionWithUser } from '@opendatacapture/schemas/session'; +import type { Session, SessionWithUserQueryParams } from '@opendatacapture/schemas/session'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; From e0d45a9a01e10daaecd7f94cc223b96d15eb83cb Mon Sep 17 00:00:00 2001 From: david-roper Date: Thu, 20 Nov 2025 17:15:35 -0500 Subject: [PATCH 42/86] feat: changed how we find sessions to useFindSessionQuery instead, test update needed --- apps/api/src/sessions/sessions.service.ts | 4 ++- apps/web/src/hooks/useFindSessionQuery.ts | 36 +++++++++++-------- .../src/hooks/useInstrumentVisualization.ts | 32 +++++++---------- packages/schemas/src/session/session.ts | 10 +++--- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index 14f4bf0f3..d3bc19a73 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -46,7 +46,9 @@ export class SessionsService { let group: Group | null = null; if (groupId && !subject.groupIds.includes(groupId)) { group = await this.groupsService.findById(groupId); - await this.subjectsService.addGroupForSubject(subject.id, group.id); + if (group) { + await this.subjectsService.addGroupForSubject(subject.id, group.id); + } } const { id } = await this.sessionModel.create({ diff --git a/apps/web/src/hooks/useFindSessionQuery.ts b/apps/web/src/hooks/useFindSessionQuery.ts index f6b027d52..be00e51d2 100644 --- a/apps/web/src/hooks/useFindSessionQuery.ts +++ b/apps/web/src/hooks/useFindSessionQuery.ts @@ -1,4 +1,4 @@ -import { $SessionWithUser } from '@opendatacapture/schemas/session'; +import { $Session, $SessionWithUser } from '@opendatacapture/schemas/session'; import type { Session, SessionWithUserQueryParams } from '@opendatacapture/schemas/session'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; @@ -14,18 +14,19 @@ type UseSessionOptions = { params: SessionWithUserQueryParams; }; -export const sessionInfo = async (sessionId: string): Promise => { - try { - const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); - if (!response.data) { - throw new Error('Session data does not exist'); - } - return response.data as Session; - } catch (error) { - console.error('Error fetching session:', error); - throw error; - } -}; +// export const sessionInfo = async (sessionId: string): Promise => { +// try { +// const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); +// const parsedData = $Session.safeParse(response.data) +// if(!parsedData.success){ +// throw new Error(parsedData.error.message); +// } +// return parsedData.data; +// } catch (error) { +// console.error('Error fetching session:', error); +// throw error; +// } +// }; export const useFindSessionQuery = ( { enabled, params }: UseSessionOptions = { @@ -36,10 +37,15 @@ export const useFindSessionQuery = ( return useQuery({ enabled, queryFn: async () => { - const response = await axios.get('/v1/sessions/', { + const response = await axios.get('/v1/sessions', { params }); - return $SessionWithUser.array().parseAsync(response.data); + const parsedData = $SessionWithUser.array().safeParseAsync(response.data); + if ((await parsedData).error) { + console.log((await parsedData).error); + throw new Error(`cant find data`); + } + return (await parsedData).data; }, queryKey: ['sessions', ...Object.values(params)] }); diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 6a98a4206..ef932b86d 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -13,7 +13,7 @@ import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; -import { sessionInfo, useFindSessionQuery } from './useFindSessionQuery'; +import { useFindSessionQuery } from './useFindSessionQuery'; import { userInfo } from './useFindUser'; type InstrumentVisualizationRecord = { @@ -57,12 +57,12 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); - // const sessionsUsernameQuery = useFindSessionQuery({ - // enabled: instrumentId !== null, - // params: { - // groupId: currentGroup?.id - // } - // }); + const sessionsUsernameQuery = useFindSessionQuery({ + enabled: instrumentId !== null, + params: { + groupId: currentGroup?.id + } + }); // Create a new sessionsUsernameQuery which uses the useFindSessionQuery hook // have use a different return type with @@ -222,24 +222,16 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio let cancelled = false; const fetchRecords = async () => { try { - if (recordsQuery.data) { + const sessions = await sessionsUsernameQuery.data; + if (recordsQuery.data && sessions) { // Fetch all sessions in parallel - const sessionPromises = recordsQuery.data.map((record) => sessionInfo(record.sessionId)); - const sessions = await Promise.all(sessionPromises); - - // Extract unique userIds and fetch users in parallel - const userIds = [...new Set(sessions.filter((s) => s?.userId).map((s) => s.userId))]; - - //assume userId exists in userId set as we already filtered out the non-existing userIds - const userPromises = userIds.map((userId) => userInfo(userId!).catch(() => null)); - const users = await Promise.all(userPromises); - const userMap = new Map(users.filter((u) => u).map((u) => [u!.id, u!.username])); // Build records with looked-up data const records: InstrumentVisualizationRecord[] = recordsQuery.data.map((record, i) => { const props = record.data && typeof record.data === 'object' ? record.data : {}; - const session = sessions[i]; - const username = session?.userId ? (userMap.get(session.userId) ?? 'N/A') : 'N/A'; + const usersSessions = sessions.filter((s) => s.id === record.sessionId); + const session = usersSessions[0]; + const username = session?.user?.username ?? 'N/A'; return { __date__: record.date, diff --git a/packages/schemas/src/session/session.ts b/packages/schemas/src/session/session.ts index 20d2a83b8..5088cd8b2 100644 --- a/packages/schemas/src/session/session.ts +++ b/packages/schemas/src/session/session.ts @@ -10,7 +10,7 @@ export type Session = z.infer; export const $Session = $BaseModel.extend({ date: z.coerce.date(), groupId: z.string().nullable(), - subject: $Subject, + subject: $Subject.nullable(), subjectId: z.string(), type: $SessionType, userId: z.string().nullish() @@ -27,9 +27,11 @@ export const $CreateSessionData = z.object({ export type SessionWithUser = z.infer; export const $SessionWithUser = $Session.extend({ - user: z.object({ - username: z.string().nullish() - }) + user: z + .object({ + username: z.string().nullish() + }) + .nullable() }); export type SessionWithUserQueryParams = { From ceb6dcbd5c25935260ff8b6dc2a882aa90b99857 Mon Sep 17 00:00:00 2001 From: David Roper Date: Fri, 21 Nov 2025 10:54:19 -0500 Subject: [PATCH 43/86] feat: cleanup unused sessionInfo method, resolve prettier issues --- apps/api/src/sessions/sessions.service.ts | 4 ++-- apps/web/src/hooks/useFindSessionQuery.ts | 19 ++----------------- .../src/hooks/useInstrumentVisualization.ts | 7 +++---- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index d3bc19a73..5c406e97c 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -99,12 +99,12 @@ export class SessionsService { async findAllIncludeUsernames(groupId?: string, { ability }: EntityOperationOptions = {}) { const sessionsWithUsers = await this.sessionModel.findMany({ include: { + subject: true, user: { select: { username: true } - }, - subject: true + } }, where: { AND: [accessibleQuery(ability, 'read', 'Session'), { groupId }] diff --git a/apps/web/src/hooks/useFindSessionQuery.ts b/apps/web/src/hooks/useFindSessionQuery.ts index be00e51d2..782cfb7ab 100644 --- a/apps/web/src/hooks/useFindSessionQuery.ts +++ b/apps/web/src/hooks/useFindSessionQuery.ts @@ -1,5 +1,5 @@ -import { $Session, $SessionWithUser } from '@opendatacapture/schemas/session'; -import type { Session, SessionWithUserQueryParams } from '@opendatacapture/schemas/session'; +import { $SessionWithUser } from '@opendatacapture/schemas/session'; +import type { SessionWithUserQueryParams } from '@opendatacapture/schemas/session'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; @@ -14,20 +14,6 @@ type UseSessionOptions = { params: SessionWithUserQueryParams; }; -// export const sessionInfo = async (sessionId: string): Promise => { -// try { -// const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); -// const parsedData = $Session.safeParse(response.data) -// if(!parsedData.success){ -// throw new Error(parsedData.error.message); -// } -// return parsedData.data; -// } catch (error) { -// console.error('Error fetching session:', error); -// throw error; -// } -// }; - export const useFindSessionQuery = ( { enabled, params }: UseSessionOptions = { enabled: true, @@ -42,7 +28,6 @@ export const useFindSessionQuery = ( }); const parsedData = $SessionWithUser.array().safeParseAsync(response.data); if ((await parsedData).error) { - console.log((await parsedData).error); throw new Error(`cant find data`); } return (await parsedData).data; diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index ef932b86d..e7910e6a4 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -14,7 +14,6 @@ import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; import { useFindSessionQuery } from './useFindSessionQuery'; -import { userInfo } from './useFindUser'; type InstrumentVisualizationRecord = { [key: string]: unknown; @@ -220,14 +219,14 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio useEffect(() => { let cancelled = false; - const fetchRecords = async () => { + const fetchRecords = () => { try { - const sessions = await sessionsUsernameQuery.data; + const sessions = sessionsUsernameQuery.data; if (recordsQuery.data && sessions) { // Fetch all sessions in parallel // Build records with looked-up data - const records: InstrumentVisualizationRecord[] = recordsQuery.data.map((record, i) => { + const records: InstrumentVisualizationRecord[] = recordsQuery.data.map((record) => { const props = record.data && typeof record.data === 'object' ? record.data : {}; const usersSessions = sessions.filter((s) => s.id === record.sessionId); const session = usersSessions[0]; From f61516145946945cd83e2399984322c727da1bfc Mon Sep 17 00:00:00 2001 From: David Roper Date: Fri, 21 Nov 2025 11:05:57 -0500 Subject: [PATCH 44/86] test: update test mocks --- .../useInstrumentVisualization.test.ts | 24 +++++++++---------- .../src/hooks/useInstrumentVisualization.ts | 8 +------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index db12e8427..697ccfe7c 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -38,13 +38,15 @@ const mockInstrumentRecords = { ] }; -const mockSession = { - userId: '111' -}; - -const mockUser = { - id: '111', - username: 'testusername' +const mockSessionWithUsername = { + data: [ + { + id: '123', + user: { + username: 'testusername' + } + } + ] }; vi.mock('@/hooks/useInstrument', () => ({ @@ -73,12 +75,8 @@ vi.mock('@/hooks/useInstrumentRecords', () => ({ useInstrumentRecords: () => mockInstrumentRecords })); -vi.mock('@/hooks/useFindSession', () => ({ - sessionInfo: () => Promise.resolve(mockSession) -})); - -vi.mock('@/hooks/useFindUser', () => ({ - userInfo: () => Promise.resolve(mockUser) +vi.mock('@/hooks/useFindSessionQuery', () => ({ + useFindSessionQuery: () => mockSessionWithUsername })); describe('useInstrumentVisualization', () => { diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index e7910e6a4..f2b63fac2 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -218,7 +218,6 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio }; useEffect(() => { - let cancelled = false; const fetchRecords = () => { try { const sessions = sessionsUsernameQuery.data; @@ -241,9 +240,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio }; }); - if (!cancelled) { - setRecords(records); - } + setRecords(records); } } catch (error) { console.error('Error occurred: ', error); @@ -257,9 +254,6 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }; void fetchRecords(); - return () => { - cancelled = true; - }; }, [recordsQuery.data]); const instrumentOptions: { [key: string]: string } = useMemo(() => { From ea093eb4eb36ff482e7c08bbc93c5a8d4b6e10fe Mon Sep 17 00:00:00 2001 From: David Roper Date: Fri, 21 Nov 2025 11:15:38 -0500 Subject: [PATCH 45/86] feat: user find method instead of filter to get 1 unique userSession --- apps/api/src/sessions/sessions.controller.ts | 2 +- apps/web/src/hooks/useInstrumentVisualization.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 0b9e50773..1ff3e2806 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -1,6 +1,7 @@ import { CurrentUser } from '@douglasneuroinformatics/libnest'; import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; import { ApiOperation } from '@nestjs/swagger'; +import type { SessionWithUser } from '@opendatacapture/schemas/session'; import type { Session } from '@prisma/client'; import type { AppAbility } from '@/auth/auth.types'; @@ -8,7 +9,6 @@ import { RouteAccess } from '@/core/decorators/route-access.decorator'; import { CreateSessionDto } from './dto/create-session.dto'; import { SessionsService } from './sessions.service'; -import type { SessionWithUser } from '@opendatacapture/schemas/session'; @Controller('sessions') export class SessionsController { diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index f2b63fac2..4fc1e72bc 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -227,9 +227,9 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio // Build records with looked-up data const records: InstrumentVisualizationRecord[] = recordsQuery.data.map((record) => { const props = record.data && typeof record.data === 'object' ? record.data : {}; - const usersSessions = sessions.filter((s) => s.id === record.sessionId); - const session = usersSessions[0]; - const username = session?.user?.username ?? 'N/A'; + const usersSession = sessions.find((s) => s.id === record.sessionId); + + const username = usersSession?.user?.username ?? 'N/A'; return { __date__: record.date, From ee70035c7649c1aebca11340aad449cbc9b835a2 Mon Sep 17 00:00:00 2001 From: David Roper Date: Fri, 21 Nov 2025 11:16:33 -0500 Subject: [PATCH 46/86] fix: adding ! to currentSession subject mentions at they should always contain a subject --- apps/web/src/components/Sidebar/Sidebar.tsx | 4 ++-- apps/web/src/routes/_app/instruments/render/$id.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/Sidebar/Sidebar.tsx b/apps/web/src/components/Sidebar/Sidebar.tsx index 1bfd11457..5799a5fc0 100644 --- a/apps/web/src/components/Sidebar/Sidebar.tsx +++ b/apps/web/src/components/Sidebar/Sidebar.tsx @@ -88,7 +88,7 @@ export const Sidebar = () => { >
{t('common.sessionInProgress')}

- {isSubjectWithPersonalInfo(currentSession.subject) ? ( + {isSubjectWithPersonalInfo(currentSession.subject!) ? (

{`${t('core.fullName')}: ${currentSession.subject.firstName} ${currentSession.subject.lastName}`}

@@ -100,7 +100,7 @@ export const Sidebar = () => {

) : (
-

ID: {removeSubjectIdScope(currentSession.subject.id)}

+

ID: {removeSubjectIdScope(currentSession.subject!.id)}

)} diff --git a/apps/web/src/routes/_app/instruments/render/$id.tsx b/apps/web/src/routes/_app/instruments/render/$id.tsx index 3b548c19c..03e88c70e 100644 --- a/apps/web/src/routes/_app/instruments/render/$id.tsx +++ b/apps/web/src/routes/_app/instruments/render/$id.tsx @@ -42,7 +42,7 @@ const RouteComponent = () => { groupId: currentGroup?.id, instrumentId, sessionId: currentSession!.id, - subjectId: currentSession!.subject.id + subjectId: currentSession!.subject!.id } satisfies CreateInstrumentRecordData); notifications.addNotification({ type: 'success' }); }; @@ -61,7 +61,7 @@ const RouteComponent = () => {
From d607dc63cb8e84015c9d03b2d3da33e2e84c5a2d Mon Sep 17 00:00:00 2001 From: david-roper Date: Fri, 24 Oct 2025 16:57:26 -0400 Subject: [PATCH 47/86] feat: create useFindSession Hook --- apps/web/src/hooks/useFindSession.ts | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 apps/web/src/hooks/useFindSession.ts diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts new file mode 100644 index 000000000..b072e5cfb --- /dev/null +++ b/apps/web/src/hooks/useFindSession.ts @@ -0,0 +1,30 @@ +import { reviver } from '@douglasneuroinformatics/libjs'; +import { $Session } from '@opendatacapture/schemas/session'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +type UseSessionOptions = { + enabled?: boolean; + params: { + id?: string; + }; +}; + +export const useFindSession = ( + { enabled, params }: UseSessionOptions = { + enabled: true, + params: {} + } +) => { + return useQuery({ + enabled, + queryFn: async () => { + const response = await axios.get('/v1/Sessions', { + params, + transformResponse: [(data: string) => JSON.parse(data, reviver) as unknown] + }); + return $Session.array().parseAsync(response.data); + }, + queryKey: ['sessions', ...Object.values(params)] + }); +}; From 7f6d4187e220910ca1ec201b8ecc673dadaf5fb1 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 27 Oct 2025 13:08:49 -0400 Subject: [PATCH 48/86] feat: make useSession hook return one session instead of array --- apps/web/src/hooks/useFindSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index b072e5cfb..8b5e5f5b3 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -23,7 +23,7 @@ export const useFindSession = ( params, transformResponse: [(data: string) => JSON.parse(data, reviver) as unknown] }); - return $Session.array().parseAsync(response.data); + return $Session.parseAsync(response.data); }, queryKey: ['sessions', ...Object.values(params)] }); From f0e9d18502bf52781f56e4862f53b72051a15fd2 Mon Sep 17 00:00:00 2001 From: David Roper Date: Mon, 27 Oct 2025 14:52:05 -0400 Subject: [PATCH 49/86] feat: add hook to return list of user ids --- apps/web/src/hooks/useInstrumentVisualization.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 4fc1e72bc..3841ac5d6 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -12,6 +12,7 @@ import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; +import { useFindSession } from './useFindSession'; import { useFindSessionQuery } from './useFindSessionQuery'; From 99397f228ce455f0385c3df8c8da11bce049a82d Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 27 Oct 2025 17:11:44 -0400 Subject: [PATCH 50/86] feat: new api reqs for finding sessions --- apps/api/src/sessions/sessions.controller.ts | 8 ++++++++ apps/api/src/sessions/sessions.service.ts | 15 +++++++++++++++ apps/web/src/hooks/useInstrumentVisualization.ts | 1 - 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 1ff3e2806..fef6bd638 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -37,4 +37,12 @@ export class SessionsController { findByID(@Param('id') id: string, @CurrentUser('ability') ability: AppAbility): Promise { return this.sessionsService.findById(id, { ability }); } + + @ApiOperation({ description: 'Find Session by ID' }) + @Post('list') + @RouteAccess({ action: 'read', subject: 'Session' }) + findSessionList(@Query('ids') ids: string[]): Promise { + const idArray = Array.isArray(ids) ? ids : (ids as string).split(','); + return this.sessionsService.findSessionList(idArray); + } } diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index 5c406e97c..0eb0fd39d 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -126,6 +126,21 @@ export class SessionsService { return session; } + async findSessionList(ids: string[], { ability }: EntityOperationOptions = {}) { + const sessionsArray = await Promise.all( + ids.map(async (id) => { + const session = await this.sessionModel.findFirst({ + where: { AND: [accessibleQuery(ability, 'read', 'Session')], id } + }); + if (!session) { + throw new NotFoundException(`Failed to find session with ID: ${id}`); + } + return session; + }) + ); + return sessionsArray; + } + /** Get the subject if they exist, otherwise create them */ private async resolveSubject(subjectData: CreateSubjectData) { this.loggingService.debug({ message: 'Attempting to resolve subject', subjectData }); diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 3841ac5d6..4fc1e72bc 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -12,7 +12,6 @@ import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; import { downloadSubjectTableExcel } from '@/utils/excel'; -import { useFindSession } from './useFindSession'; import { useFindSessionQuery } from './useFindSessionQuery'; From eafc5e484cf44cb478318003c29d9b602773de1f Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 10 Nov 2025 14:13:38 -0500 Subject: [PATCH 51/86] refactor: remove unused useFindSession hook --- apps/web/src/hooks/useFindSession.ts | 30 ---------------------------- 1 file changed, 30 deletions(-) delete mode 100644 apps/web/src/hooks/useFindSession.ts diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts deleted file mode 100644 index 8b5e5f5b3..000000000 --- a/apps/web/src/hooks/useFindSession.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { reviver } from '@douglasneuroinformatics/libjs'; -import { $Session } from '@opendatacapture/schemas/session'; -import { useQuery } from '@tanstack/react-query'; -import axios from 'axios'; - -type UseSessionOptions = { - enabled?: boolean; - params: { - id?: string; - }; -}; - -export const useFindSession = ( - { enabled, params }: UseSessionOptions = { - enabled: true, - params: {} - } -) => { - return useQuery({ - enabled, - queryFn: async () => { - const response = await axios.get('/v1/Sessions', { - params, - transformResponse: [(data: string) => JSON.parse(data, reviver) as unknown] - }); - return $Session.parseAsync(response.data); - }, - queryKey: ['sessions', ...Object.values(params)] - }); -}; From cd05322ae911d8da5a2d9890acfcd1cfe1920c6f Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 12 Nov 2025 10:02:27 -0500 Subject: [PATCH 52/86] feat: collect username with user api call --- apps/web/src/hooks/useInstrumentVisualization.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 4fc1e72bc..fe2618ca3 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -66,6 +66,16 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio // Create a new sessionsUsernameQuery which uses the useFindSessionQuery hook // have use a different return type with + const userInfo = async (userId: string): Promise => { + try { + const response = await axios.get(`/v1/users/${userId}`); + return response.data ? (response.data as User) : null; + } catch (error) { + console.error('Error fetching user:', error); + return null; // ensures a resolved value instead of `void` + } + }; + const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); From b7201be616359828719cdb51f5be0ac1a57418e4 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 12 Nov 2025 16:28:58 -0500 Subject: [PATCH 53/86] refactor: move session and user api methods to separate hook files --- apps/web/src/hooks/useFindSession.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 apps/web/src/hooks/useFindSession.ts diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts new file mode 100644 index 000000000..e3ae1592c --- /dev/null +++ b/apps/web/src/hooks/useFindSession.ts @@ -0,0 +1,12 @@ +import type { Session } from '@opendatacapture/schemas/session'; +import axios from 'axios'; + +export const sessionInfo = async (sessionId: string): Promise => { + try { + const response = await axios.get(`/v1/sessions/${sessionId}`); + return response.data ? (response.data as Session) : null; + } catch (error) { + console.error('Error fetching user:', error); + return null; // ensures a resolved value instead of `void` + } +}; From 0bf25b39d31e5f1e42e48b57ee09f7fe1d1b7962 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 12 Nov 2025 16:29:27 -0500 Subject: [PATCH 54/86] feat: create mocks for findUser and findSession hooks --- .../hooks/__tests__/useInstrumentVisualization.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index 697ccfe7c..acf5be5b1 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -49,6 +49,14 @@ const mockSessionWithUsername = { ] }; +const mockSession = { + sessionId: 123 +}; + +const mockUser = { + username: 'testusername' +}; + vi.mock('@/hooks/useInstrument', () => ({ useInstrument: mockUseInstrument })); From c9bf879033bb89628ac3340b6037292cc3ff88d2 Mon Sep 17 00:00:00 2001 From: David Roper Date: Thu, 13 Nov 2025 11:52:13 -0500 Subject: [PATCH 55/86] test: fix use tests with wait for methods --- .../hooks/__tests__/useInstrumentVisualization.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index acf5be5b1..697ccfe7c 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -49,14 +49,6 @@ const mockSessionWithUsername = { ] }; -const mockSession = { - sessionId: 123 -}; - -const mockUser = { - username: 'testusername' -}; - vi.mock('@/hooks/useInstrument', () => ({ useInstrument: mockUseInstrument })); From 91521d488977fdb4ffe009d3f83c5f6b4f6368b7 Mon Sep 17 00:00:00 2001 From: david-roper Date: Fri, 14 Nov 2025 11:27:23 -0500 Subject: [PATCH 56/86] refactor: make Username a standalone column in long export formats --- apps/web/src/hooks/useInstrumentVisualization.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index fe2618ca3..fefef48ce 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -138,6 +138,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio GroupID: currentGroup ? currentGroup.id : 'root', // eslint-disable-next-line perfectionist/sort-objects Date: toBasicISOString(date), + Username: username, SubjectID: removeSubjectIdScope(params.subjectId), Username: username, Variable: `${objKey}-${arrKey}`, @@ -151,6 +152,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio GroupID: currentGroup ? currentGroup.id : 'root', // eslint-disable-next-line perfectionist/sort-objects Date: toBasicISOString(date), + Username: username, SubjectID: removeSubjectIdScope(params.subjectId), Username: username, Value: objVal, From 47e5d661328728e48bfe488f33a2e99426969fa3 Mon Sep 17 00:00:00 2001 From: david-roper Date: Fri, 14 Nov 2025 15:07:47 -0500 Subject: [PATCH 57/86] chore: small changes to test From c3703ab63c896c3b6f6bf56883019547e24abecf Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 11:37:01 -0500 Subject: [PATCH 58/86] fix: fix description in session controller --- apps/api/src/sessions/sessions.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index fef6bd638..bf277d012 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -38,7 +38,7 @@ export class SessionsController { return this.sessionsService.findById(id, { ability }); } - @ApiOperation({ description: 'Find Session by ID' }) + @ApiOperation({ description: 'Find Sessions by ID' }) @Post('list') @RouteAccess({ action: 'read', subject: 'Session' }) findSessionList(@Query('ids') ids: string[]): Promise { From d6d4d0bd3203abc54231fea07082a13db9480713 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 11:38:52 -0500 Subject: [PATCH 59/86] fix: add ability to find sessions list --- apps/api/src/sessions/sessions.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index bf277d012..ccd2b939d 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -41,8 +41,8 @@ export class SessionsController { @ApiOperation({ description: 'Find Sessions by ID' }) @Post('list') @RouteAccess({ action: 'read', subject: 'Session' }) - findSessionList(@Query('ids') ids: string[]): Promise { + findSessionList(@Query('ids') ids: string[], @CurrentUser('ability') ability: AppAbility): Promise { const idArray = Array.isArray(ids) ? ids : (ids as string).split(','); - return this.sessionsService.findSessionList(idArray); + return this.sessionsService.findSessionList(idArray, { ability }); } } From 3cf235860118d71b98b76ccec25e04d739e6c1c4 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 11:58:21 -0500 Subject: [PATCH 60/86] fix: error msg in usefindsession --- apps/web/src/hooks/useFindSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index e3ae1592c..08d3ad54e 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -6,7 +6,7 @@ export const sessionInfo = async (sessionId: string): Promise => const response = await axios.get(`/v1/sessions/${sessionId}`); return response.data ? (response.data as Session) : null; } catch (error) { - console.error('Error fetching user:', error); + console.error('Error fetching session:', error); return null; // ensures a resolved value instead of `void` } }; From 90b6abcfe518e051b5a2b01493bf52f775039d97 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 14:42:17 -0500 Subject: [PATCH 61/86] fix: throw and error to catch instead of returning null when error occurs --- apps/web/src/hooks/useFindSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 08d3ad54e..4232834bb 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -7,6 +7,6 @@ export const sessionInfo = async (sessionId: string): Promise => return response.data ? (response.data as Session) : null; } catch (error) { console.error('Error fetching session:', error); - return null; // ensures a resolved value instead of `void` + throw error; } }; From 72c0a2afa67b6e158f7d382fe5fef9ca682ee9cc Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 14:58:26 -0500 Subject: [PATCH 62/86] feat: use schema parsing to confirm contents instead of casting it --- apps/web/src/hooks/useFindSession.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 4232834bb..1360a5b66 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,10 +1,11 @@ -import type { Session } from '@opendatacapture/schemas/session'; +import { type Session, $Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${sessionId}`); - return response.data ? (response.data as Session) : null; + const parsedResult = $Session.safeParse(response.data); + return parsedResult.success ? parsedResult.data : null; } catch (error) { console.error('Error fetching session:', error); throw error; From 52828a914df210f19dc10d057282aabda344ba3d Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 15:49:03 -0500 Subject: [PATCH 63/86] fix: fix type exports --- apps/web/src/hooks/useFindSession.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 1360a5b66..2c838cf2a 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,4 +1,5 @@ -import { type Session, $Session } from '@opendatacapture/schemas/session'; +import { $Session } from '@opendatacapture/schemas/session'; +import type { Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { From 6c72e3585258a0062e1d20e9444e039ed9bc2de9 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 16:09:25 -0500 Subject: [PATCH 64/86] test: change positions of subjectId and username column to make linter happy with itself --- apps/web/src/hooks/useInstrumentVisualization.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index fefef48ce..fe2618ca3 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -138,7 +138,6 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio GroupID: currentGroup ? currentGroup.id : 'root', // eslint-disable-next-line perfectionist/sort-objects Date: toBasicISOString(date), - Username: username, SubjectID: removeSubjectIdScope(params.subjectId), Username: username, Variable: `${objKey}-${arrKey}`, @@ -152,7 +151,6 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio GroupID: currentGroup ? currentGroup.id : 'root', // eslint-disable-next-line perfectionist/sort-objects Date: toBasicISOString(date), - Username: username, SubjectID: removeSubjectIdScope(params.subjectId), Username: username, Value: objVal, From bccf67a327d0191ebe9bd9694defd79da038db3c Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 17 Nov 2025 16:09:50 -0500 Subject: [PATCH 65/86] refactor: remove unused api call --- apps/api/src/sessions/sessions.controller.ts | 8 -------- apps/api/src/sessions/sessions.service.ts | 15 --------------- 2 files changed, 23 deletions(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index ccd2b939d..1ff3e2806 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -37,12 +37,4 @@ export class SessionsController { findByID(@Param('id') id: string, @CurrentUser('ability') ability: AppAbility): Promise { return this.sessionsService.findById(id, { ability }); } - - @ApiOperation({ description: 'Find Sessions by ID' }) - @Post('list') - @RouteAccess({ action: 'read', subject: 'Session' }) - findSessionList(@Query('ids') ids: string[], @CurrentUser('ability') ability: AppAbility): Promise { - const idArray = Array.isArray(ids) ? ids : (ids as string).split(','); - return this.sessionsService.findSessionList(idArray, { ability }); - } } diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index 0eb0fd39d..5c406e97c 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -126,21 +126,6 @@ export class SessionsService { return session; } - async findSessionList(ids: string[], { ability }: EntityOperationOptions = {}) { - const sessionsArray = await Promise.all( - ids.map(async (id) => { - const session = await this.sessionModel.findFirst({ - where: { AND: [accessibleQuery(ability, 'read', 'Session')], id } - }); - if (!session) { - throw new NotFoundException(`Failed to find session with ID: ${id}`); - } - return session; - }) - ); - return sessionsArray; - } - /** Get the subject if they exist, otherwise create them */ private async resolveSubject(subjectData: CreateSubjectData) { this.loggingService.debug({ message: 'Attempting to resolve subject', subjectData }); From 182463a576cf58608092731b924fc0e01e027c02 Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 14:35:32 -0500 Subject: [PATCH 66/86] feat: fix not finding user id issue by making subject inclusion optional --- apps/web/src/hooks/useFindSession.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 2c838cf2a..ffc1fb7fe 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -5,8 +5,8 @@ import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${sessionId}`); - const parsedResult = $Session.safeParse(response.data); - return parsedResult.success ? parsedResult.data : null; + const parsedResult = await $Session.parseAsync(response.data); + return parsedResult; } catch (error) { console.error('Error fetching session:', error); throw error; From 895197b301eeb304481d7b0d6264e08cec3ea76b Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 14:44:59 -0500 Subject: [PATCH 67/86] chore: revert session schema and parse change --- apps/web/src/hooks/useFindSession.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index ffc1fb7fe..01fb77508 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,12 +1,13 @@ -import { $Session } from '@opendatacapture/schemas/session'; import type { Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${sessionId}`); - const parsedResult = await $Session.parseAsync(response.data); - return parsedResult; + if (!response.data) { + throw new Error('Session data does not exist'); + } + return response.data as Session; } catch (error) { console.error('Error fetching session:', error); throw error; From 7c02666149d8b7da5e4dc9f64df7ac6d7242dc1f Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 18 Nov 2025 15:06:24 -0500 Subject: [PATCH 68/86] feat: remove redundant null return type from useFindSession --- apps/web/src/hooks/useFindSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 01fb77508..676c908fc 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,7 +1,7 @@ import type { Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; -export const sessionInfo = async (sessionId: string): Promise => { +export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${sessionId}`); if (!response.data) { From 25903b02cc64ce8239553c3c062fe3c066a4d783 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 19 Nov 2025 10:16:35 -0500 Subject: [PATCH 69/86] chore: and encodeUriComponent to ids --- apps/web/src/hooks/useFindSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 676c908fc..333890c62 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -3,7 +3,7 @@ import axios from 'axios'; export const sessionInfo = async (sessionId: string): Promise => { try { - const response = await axios.get(`/v1/sessions/${sessionId}`); + const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); if (!response.data) { throw new Error('Session data does not exist'); } From 05ba9dedd70ae2eee99e0d91b8c5af0f34211f25 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 19 Nov 2025 11:35:17 -0500 Subject: [PATCH 70/86] feat: add cancelled var to avoid race conditions in fetch records --- .../src/hooks/useInstrumentVisualization.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index fe2618ca3..9caa4a855 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -63,18 +63,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); - // Create a new sessionsUsernameQuery which uses the useFindSessionQuery hook - // have use a different return type with - - const userInfo = async (userId: string): Promise => { - try { - const response = await axios.get(`/v1/users/${userId}`); - return response.data ? (response.data as User) : null; - } catch (error) { - console.error('Error fetching user:', error); - return null; // ensures a resolved value instead of `void` - } - }; + const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { @@ -250,7 +239,9 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio }; }); - setRecords(records); + if (!cancelled) { + setRecords(records); + } } } catch (error) { console.error('Error occurred: ', error); @@ -264,6 +255,9 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }; void fetchRecords(); + return () => { + cancelled = true; + }; }, [recordsQuery.data]); const instrumentOptions: { [key: string]: string } = useMemo(() => { From bcd84a5a95f3f48e476ee8bed1cbd7b24a3ad95a Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 19 Nov 2025 17:06:02 -0500 Subject: [PATCH 71/86] feat: add new findAllSessionsIncludeUsernames api call and todo comments --- apps/web/src/hooks/useFindSession.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts index 333890c62..a3aac0c34 100644 --- a/apps/web/src/hooks/useFindSession.ts +++ b/apps/web/src/hooks/useFindSession.ts @@ -1,6 +1,12 @@ import type { Session } from '@opendatacapture/schemas/session'; import axios from 'axios'; +//Change this query to into a hook method and name it useFindSessionQuery + +//Change the api call to have an include tag which includes the username from users + +//Change the return type to + export const sessionInfo = async (sessionId: string): Promise => { try { const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); From 8b4bed1e39e81df97b68001db28d13de9dc549ce Mon Sep 17 00:00:00 2001 From: david-roper Date: Thu, 20 Nov 2025 13:40:04 -0500 Subject: [PATCH 72/86] feat: rename function to useFindSessionQuery --- apps/web/src/hooks/useFindSession.ts | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 apps/web/src/hooks/useFindSession.ts diff --git a/apps/web/src/hooks/useFindSession.ts b/apps/web/src/hooks/useFindSession.ts deleted file mode 100644 index a3aac0c34..000000000 --- a/apps/web/src/hooks/useFindSession.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Session } from '@opendatacapture/schemas/session'; -import axios from 'axios'; - -//Change this query to into a hook method and name it useFindSessionQuery - -//Change the api call to have an include tag which includes the username from users - -//Change the return type to - -export const sessionInfo = async (sessionId: string): Promise => { - try { - const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`); - if (!response.data) { - throw new Error('Session data does not exist'); - } - return response.data as Session; - } catch (error) { - console.error('Error fetching session:', error); - throw error; - } -}; From c35549bfbbcb0b1c8cb817286f41f85011905010 Mon Sep 17 00:00:00 2001 From: david-roper Date: Thu, 20 Nov 2025 13:41:40 -0500 Subject: [PATCH 73/86] feat: update types of findAllIncludeUsernames --- apps/api/src/sessions/sessions.controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 1ff3e2806..59ed4c8af 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -9,6 +9,7 @@ import { RouteAccess } from '@/core/decorators/route-access.decorator'; import { CreateSessionDto } from './dto/create-session.dto'; import { SessionsService } from './sessions.service'; +import type { SessionWithUser } from '@opendatacapture/schemas/session'; @Controller('sessions') export class SessionsController { From 8b2601505ca39b9956102e9c3da5eeef2b9deb4f Mon Sep 17 00:00:00 2001 From: david-roper Date: Thu, 20 Nov 2025 15:41:43 -0500 Subject: [PATCH 74/86] feat: update query and type imports --- apps/api/src/sessions/sessions.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index 5c406e97c..e8b362103 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -104,7 +104,8 @@ export class SessionsService { select: { username: true } - } + }, + subject: true }, where: { AND: [accessibleQuery(ability, 'read', 'Session'), { groupId }] From 623c2e989fcf8918fc4806c25169d69dfda3447c Mon Sep 17 00:00:00 2001 From: David Roper <60201980+david-roper@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:25:23 -0500 Subject: [PATCH 75/86] Remove duplicate import for SessionWithUser type --- apps/api/src/sessions/sessions.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/api/src/sessions/sessions.controller.ts b/apps/api/src/sessions/sessions.controller.ts index 59ed4c8af..1ff3e2806 100644 --- a/apps/api/src/sessions/sessions.controller.ts +++ b/apps/api/src/sessions/sessions.controller.ts @@ -9,7 +9,6 @@ import { RouteAccess } from '@/core/decorators/route-access.decorator'; import { CreateSessionDto } from './dto/create-session.dto'; import { SessionsService } from './sessions.service'; -import type { SessionWithUser } from '@opendatacapture/schemas/session'; @Controller('sessions') export class SessionsController { From 688887f49139f21e7101ae63ef5c0a1c5f741582 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 26 Nov 2025 11:53:21 -0500 Subject: [PATCH 76/86] fix: use zod error message for useFindSessionQuery --- apps/web/src/hooks/useFindSessionQuery.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useFindSessionQuery.ts b/apps/web/src/hooks/useFindSessionQuery.ts index 782cfb7ab..238c5503c 100644 --- a/apps/web/src/hooks/useFindSessionQuery.ts +++ b/apps/web/src/hooks/useFindSessionQuery.ts @@ -28,7 +28,8 @@ export const useFindSessionQuery = ( }); const parsedData = $SessionWithUser.array().safeParseAsync(response.data); if ((await parsedData).error) { - throw new Error(`cant find data`); + const message = (await parsedData).error?.message; + throw new Error(message); } return (await parsedData).data; }, From 0e2f80e16dd1c2798a50de1fe6a8d750c106f99a Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 26 Nov 2025 12:03:59 -0500 Subject: [PATCH 77/86] fix: remove unused parts of tests, remove unused userInfo method --- apps/web/src/hooks/useFindUser.ts | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 apps/web/src/hooks/useFindUser.ts diff --git a/apps/web/src/hooks/useFindUser.ts b/apps/web/src/hooks/useFindUser.ts deleted file mode 100644 index 6dc2a95d1..000000000 --- a/apps/web/src/hooks/useFindUser.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { $User } from '@opendatacapture/schemas/user'; -import type { User } from '@opendatacapture/schemas/user'; -import axios from 'axios'; - -export const userInfo = async (userId: string): Promise => { - try { - const response = await axios.get(`/v1/users/${encodeURIComponent(userId)}`); - const parsedResult = $User.safeParse(response.data); - return parsedResult.success ? parsedResult.data : null; - } catch (error) { - console.error('Error fetching user:', error); - throw error; - } -}; From a98c43ff16bfacb2ad8beda324ce6d9d0d86bb4a Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 26 Nov 2025 13:27:32 -0500 Subject: [PATCH 78/86] chore: add linter format changes --- apps/api/src/sessions/sessions.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index e8b362103..5c406e97c 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -104,8 +104,7 @@ export class SessionsService { select: { username: true } - }, - subject: true + } }, where: { AND: [accessibleQuery(ability, 'read', 'Session'), { groupId }] From db53c164ba8cfcaf3766829d5bc651d884107072 Mon Sep 17 00:00:00 2001 From: David Roper Date: Wed, 26 Nov 2025 15:18:40 -0500 Subject: [PATCH 79/86] chore: update parse to parseAsync in useFindSessionQuery --- apps/web/src/hooks/useFindSessionQuery.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/web/src/hooks/useFindSessionQuery.ts b/apps/web/src/hooks/useFindSessionQuery.ts index 238c5503c..76b50fdba 100644 --- a/apps/web/src/hooks/useFindSessionQuery.ts +++ b/apps/web/src/hooks/useFindSessionQuery.ts @@ -26,12 +26,7 @@ export const useFindSessionQuery = ( const response = await axios.get('/v1/sessions', { params }); - const parsedData = $SessionWithUser.array().safeParseAsync(response.data); - if ((await parsedData).error) { - const message = (await parsedData).error?.message; - throw new Error(message); - } - return (await parsedData).data; + return $SessionWithUser.array().parseAsync(response.data); }, queryKey: ['sessions', ...Object.values(params)] }); From 4ab6ac011f2cf663fe045034a0206b73068e194d Mon Sep 17 00:00:00 2001 From: David Roper Date: Thu, 27 Nov 2025 10:15:40 -0500 Subject: [PATCH 80/86] fix: removed cancelled var --- apps/web/src/hooks/useInstrumentVisualization.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 9caa4a855..ce3ad298a 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -63,8 +63,6 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); - - const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); @@ -239,9 +237,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio }; }); - if (!cancelled) { - setRecords(records); - } + setRecords(records); } } catch (error) { console.error('Error occurred: ', error); @@ -255,9 +251,6 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }; void fetchRecords(); - return () => { - cancelled = true; - }; }, [recordsQuery.data]); const instrumentOptions: { [key: string]: string } = useMemo(() => { From 7455698d0dd4c6ff0eac38fad0f39385daca16ad Mon Sep 17 00:00:00 2001 From: David Roper <60201980+david-roper@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:47:43 -0500 Subject: [PATCH 81/86] Update apps/web/src/hooks/useFindSessionQuery.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/web/src/hooks/useFindSessionQuery.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/web/src/hooks/useFindSessionQuery.ts b/apps/web/src/hooks/useFindSessionQuery.ts index 76b50fdba..347f43bb1 100644 --- a/apps/web/src/hooks/useFindSessionQuery.ts +++ b/apps/web/src/hooks/useFindSessionQuery.ts @@ -3,11 +3,7 @@ import type { SessionWithUserQueryParams } from '@opendatacapture/schemas/sessio import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; -//Change this query to into a hook method and name it useFindSessionQuery - -//Change the api call to have an include tag which includes the username from users - -//Change the return type to +type UseSessionOptions = { type UseSessionOptions = { enabled?: boolean; From 51e1579aa00e34c730bbca7620d4d0d023db2373 Mon Sep 17 00:00:00 2001 From: David Roper Date: Thu, 27 Nov 2025 10:52:48 -0500 Subject: [PATCH 82/86] fix: remove extra brackets --- apps/web/src/hooks/useFindSessionQuery.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/src/hooks/useFindSessionQuery.ts b/apps/web/src/hooks/useFindSessionQuery.ts index 347f43bb1..a2ca85872 100644 --- a/apps/web/src/hooks/useFindSessionQuery.ts +++ b/apps/web/src/hooks/useFindSessionQuery.ts @@ -3,8 +3,6 @@ import type { SessionWithUserQueryParams } from '@opendatacapture/schemas/sessio import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; -type UseSessionOptions = { - type UseSessionOptions = { enabled?: boolean; params: SessionWithUserQueryParams; From 2e4a766c53e5f76823f5d8c4a8a1e0e333ab7640 Mon Sep 17 00:00:00 2001 From: David Roper Date: Thu, 27 Nov 2025 11:36:05 -0500 Subject: [PATCH 83/86] chore: rename userId in datahub export column to username --- apps/api/src/instrument-records/instrument-records.service.ts | 4 ++-- packages/schemas/src/instrument-records/instrument-records.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/src/instrument-records/instrument-records.service.ts b/apps/api/src/instrument-records/instrument-records.service.ts index 0830cd7e3..4d0dcbc35 100644 --- a/apps/api/src/instrument-records/instrument-records.service.ts +++ b/apps/api/src/instrument-records/instrument-records.service.ts @@ -186,7 +186,7 @@ export class InstrumentRecordsService { subjectId: removeSubjectIdScope(record.subject.id), subjectSex: record.subject.sex, timestamp: record.date.toISOString(), - userId: record.session.user?.username ?? 'N/A', + username: record.session.user?.username ?? 'N/A', value: measureValue }); } @@ -210,7 +210,7 @@ export class InstrumentRecordsService { subjectId: removeSubjectIdScope(record.subject.id), subjectSex: record.subject.sex, timestamp: record.date.toISOString(), - userId: record.session.user?.username ?? 'N/A', + username: record.session.user?.username ?? 'N/A', value: arrayEntry.measureValue }); }); diff --git a/packages/schemas/src/instrument-records/instrument-records.ts b/packages/schemas/src/instrument-records/instrument-records.ts index 019aaa466..dbeffb6bf 100644 --- a/packages/schemas/src/instrument-records/instrument-records.ts +++ b/packages/schemas/src/instrument-records/instrument-records.ts @@ -64,7 +64,7 @@ export type InstrumentRecordsExport = { subjectId: string; subjectSex: null | string; timestamp: string; - userId: string; + username: string; value: InstrumentMeasureValue; }[]; From 01d5e0c47b1c40405f51a33ce0b436ae7c24dc56 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 1 Dec 2025 12:48:41 -0500 Subject: [PATCH 84/86] chore: remove redundant fetchRecords and console error code --- .../src/hooks/useInstrumentVisualization.ts | 64 +++++++++---------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index ce3ad298a..d4e478ca9 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -215,42 +215,38 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio }; useEffect(() => { - const fetchRecords = () => { - try { - const sessions = sessionsUsernameQuery.data; - if (recordsQuery.data && sessions) { - // Fetch all sessions in parallel - - // Build records with looked-up data - const records: InstrumentVisualizationRecord[] = recordsQuery.data.map((record) => { - const props = record.data && typeof record.data === 'object' ? record.data : {}; - const usersSession = sessions.find((s) => s.id === record.sessionId); - - const username = usersSession?.user?.username ?? 'N/A'; - - return { - __date__: record.date, - __time__: record.date.getTime(), - username: username, - ...record.computedMeasures, - ...props - }; - }); - - setRecords(records); - } - } catch (error) { - console.error('Error occurred: ', error); - notifications.addNotification({ - message: t({ - en: 'Error occurred finding records', - fr: "Une erreur s'est produite lors de la recherche des enregistrements." - }), - type: 'error' + try { + const sessions = sessionsUsernameQuery.data; + if (recordsQuery.data && sessions) { + // Fetch all sessions in parallel + + // Build records with looked-up data + const records: InstrumentVisualizationRecord[] = recordsQuery.data.map((record) => { + const props = record.data && typeof record.data === 'object' ? record.data : {}; + const usersSession = sessions.find((s) => s.id === record.sessionId); + + const username = usersSession?.user?.username ?? 'N/A'; + + return { + __date__: record.date, + __time__: record.date.getTime(), + username: username, + ...record.computedMeasures, + ...props + }; }); + + setRecords(records); } - }; - void fetchRecords(); + } catch (error) { + notifications.addNotification({ + message: t({ + en: 'Error occurred finding records', + fr: "Une erreur s'est produite lors de la recherche des enregistrements." + }), + type: 'error' + }); + } }, [recordsQuery.data]); const instrumentOptions: { [key: string]: string } = useMemo(() => { From f6879262c8c12b5b8786e239d33b0b544280b2b3 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 1 Dec 2025 12:54:51 -0500 Subject: [PATCH 85/86] fix: make session service return error upon empty array --- apps/api/src/sessions/sessions.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/sessions/sessions.service.ts b/apps/api/src/sessions/sessions.service.ts index 5c406e97c..2b0889424 100644 --- a/apps/api/src/sessions/sessions.service.ts +++ b/apps/api/src/sessions/sessions.service.ts @@ -110,7 +110,7 @@ export class SessionsService { AND: [accessibleQuery(ability, 'read', 'Session'), { groupId }] } }); - if (!sessionsWithUsers) { + if (sessionsWithUsers.length < 1) { throw new NotFoundException(`Failed to find users`); } return sessionsWithUsers; From cea17b2b7cc298eb95bdaddae76669b692f79e00 Mon Sep 17 00:00:00 2001 From: david-roper Date: Mon, 1 Dec 2025 13:16:32 -0500 Subject: [PATCH 86/86] fix: keep console.error --- apps/web/src/hooks/useInstrumentVisualization.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index d4e478ca9..8da1fe142 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -239,6 +239,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio setRecords(records); } } catch (error) { + console.error(error); notifications.addNotification({ message: t({ en: 'Error occurred finding records',