Skip to content

Commit e0305a0

Browse files
authored
Merge pull request #1253 from joshunrau/new-features
New features
2 parents 1cdf215 + 861d4c2 commit e0305a0

File tree

16 files changed

+298
-20
lines changed

16 files changed

+298
-20
lines changed

apps/api/src/instrument-records/instrument-records.controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,11 @@ export class InstrumentRecordsController {
9292
) {
9393
return this.instrumentRecordsService.updateById(id, data, { ability });
9494
}
95+
96+
@ApiOperation({ summary: 'Get Instrument Record' })
97+
@Get(':id')
98+
@RouteAccess({ action: 'read', subject: 'InstrumentRecord' })
99+
findById(@Param('id', ValidObjectIdPipe) id: string, @CurrentUser('ability') ability: AppAbility) {
100+
return this.instrumentRecordsService.findById(id, { ability });
101+
}
95102
}

apps/api/src/instrument-records/instrument-records.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,16 @@ export class InstrumentRecordsService {
254254
return records;
255255
}
256256

257+
async findById(id: string, { ability }: EntityOperationOptions = {}) {
258+
const record = await this.instrumentRecordModel.findFirst({
259+
where: { AND: [accessibleQuery(ability, 'read', 'InstrumentRecord')], id }
260+
});
261+
if (!record) {
262+
throw new NotFoundException();
263+
}
264+
return record;
265+
}
266+
257267
async linearModel(
258268
{ groupId, instrumentId }: { groupId?: string; instrumentId: string },
259269
{ ability }: EntityOperationOptions = {}

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"lucide-react": "^0.507.0",
4747
"motion": "catalog:",
4848
"papaparse": "workspace:papaparse__5.x@*",
49+
"qrcode": "^1.5.4",
4950
"react": "workspace:react__19.x@*",
5051
"react-dom": "workspace:react-dom__19.x@*",
5152
"recharts": "^2.15.2",
@@ -65,6 +66,7 @@
6566
"@tanstack/router-plugin": "^1.127.3",
6667
"@testing-library/dom": "^10.4.0",
6768
"@testing-library/react": "16.2.0",
69+
"@types/qrcode": "^1.5.6",
6870
"@vitejs/plugin-react-swc": "^3.9.0",
6971
"happy-dom": "catalog:",
7072
"tailwindcss": "catalog:",

apps/web/src/components/QRCode.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useEffect, useRef } from 'react';
2+
3+
import { useTheme } from '@douglasneuroinformatics/libui/hooks';
4+
import qrcode from 'qrcode';
5+
6+
export const QRCode = ({ url }: { url: string }) => {
7+
const ref = useRef<HTMLCanvasElement>(null);
8+
9+
const [theme] = useTheme();
10+
11+
useEffect(() => {
12+
qrcode.toCanvas(
13+
ref.current,
14+
url,
15+
{
16+
color: {
17+
dark: theme === 'dark' ? '#f1f5f9' : '#0f172a',
18+
light: '#0000'
19+
},
20+
margin: 2,
21+
scale: 6
22+
},
23+
(error) => {
24+
if (error) {
25+
console.error(error);
26+
}
27+
}
28+
);
29+
}, [theme, url]);
30+
31+
return <canvas className="rounded-md border" ref={ref} />;
32+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { $InstrumentRecord } from '@opendatacapture/schemas/instrument-records';
2+
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
3+
import axios from 'axios';
4+
5+
export const instrumentRecordQueryOptions = ({ params }: { params: { id: string } }) => {
6+
return queryOptions({
7+
queryFn: async () => {
8+
const response = await axios.get(`/v1/instrument-records/${params.id}`);
9+
return $InstrumentRecord.parse(response.data);
10+
},
11+
queryKey: ['instrument-records', `id-${params.id}`]
12+
});
13+
};
14+
15+
export function useInstrumentRecordQuery({ params }: { params: { id: string } }) {
16+
return useSuspenseQuery(instrumentRecordQueryOptions({ params }));
17+
}

apps/web/src/hooks/useInstrumentVisualization.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useFindSessionQuery } from './useFindSessionQuery';
1818
type InstrumentVisualizationRecord = {
1919
[key: string]: unknown;
2020
__date__: Date;
21+
__id__: string;
2122
__time__: number;
2223
};
2324

@@ -76,7 +77,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio
7677
instrument.internal.edition
7778
}_${new Date().toISOString()}`;
7879

79-
const exportRecords = records.map((record) => omit(record, ['__time__']));
80+
const exportRecords = records.map((record) => omit(record, ['__time__', '__id__']));
8081

8182
const makeWideRows = () => {
8283
const columnNames = Object.keys(exportRecords[0]!);
@@ -229,6 +230,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio
229230

230231
return {
231232
__date__: record.date,
233+
__id__: record.id,
232234
__time__: record.date.getTime(),
233235
username: username,
234236
...record.computedMeasures,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { $Subject } from '@opendatacapture/schemas/subject';
2+
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
3+
import axios from 'axios';
4+
5+
type SubjectQueryParams = {
6+
id: string;
7+
};
8+
9+
export const subjectQueryOptions = ({ params }: { params: SubjectQueryParams }) => {
10+
return queryOptions({
11+
queryFn: async () => {
12+
const response = await axios.get(`/v1/subjects/${params.id}`, { params });
13+
return $Subject.parseAsync(response.data);
14+
},
15+
queryKey: ['subjects', `id-${params.id}`]
16+
});
17+
};
18+
19+
export function useSubjectQuery({ params }: { params: SubjectQueryParams }) {
20+
return useSuspenseQuery(subjectQueryOptions({ params }));
21+
}

apps/web/src/route-tree.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { Route as AppInstrumentsRenderIdRouteImport } from './routes/_app/instru
3131
import { Route as AppDatahubSubjectIdTableRouteImport } from './routes/_app/datahub/$subjectId/table'
3232
import { Route as AppDatahubSubjectIdGraphRouteImport } from './routes/_app/datahub/$subjectId/graph'
3333
import { Route as AppDatahubSubjectIdAssignmentsRouteImport } from './routes/_app/datahub/$subjectId/assignments'
34+
import { Route as AppDatahubSubjectIdRecordIdRouteImport } from './routes/_app/datahub/$subjectId/$recordId'
3435
import { Route as AppAdminUsersCreateRouteImport } from './routes/_app/admin/users/create'
3536
import { Route as AppAdminGroupsCreateRouteImport } from './routes/_app/admin/groups/create'
3637

@@ -148,6 +149,12 @@ const AppDatahubSubjectIdAssignmentsRoute =
148149
path: '/assignments',
149150
getParentRoute: () => AppDatahubSubjectIdRouteRoute,
150151
} as any)
152+
const AppDatahubSubjectIdRecordIdRoute =
153+
AppDatahubSubjectIdRecordIdRouteImport.update({
154+
id: '/$recordId',
155+
path: '/$recordId',
156+
getParentRoute: () => AppDatahubSubjectIdRouteRoute,
157+
} as any)
151158
const AppAdminUsersCreateRoute = AppAdminUsersCreateRouteImport.update({
152159
id: '/admin/users/create',
153160
path: '/admin/users/create',
@@ -177,6 +184,7 @@ export interface FileRoutesByFullPath {
177184
'/upload': typeof AppUploadIndexRoute
178185
'/admin/groups/create': typeof AppAdminGroupsCreateRoute
179186
'/admin/users/create': typeof AppAdminUsersCreateRoute
187+
'/datahub/$subjectId/$recordId': typeof AppDatahubSubjectIdRecordIdRoute
180188
'/datahub/$subjectId/assignments': typeof AppDatahubSubjectIdAssignmentsRoute
181189
'/datahub/$subjectId/graph': typeof AppDatahubSubjectIdGraphRoute
182190
'/datahub/$subjectId/table': typeof AppDatahubSubjectIdTableRoute
@@ -202,6 +210,7 @@ export interface FileRoutesByTo {
202210
'/upload': typeof AppUploadIndexRoute
203211
'/admin/groups/create': typeof AppAdminGroupsCreateRoute
204212
'/admin/users/create': typeof AppAdminUsersCreateRoute
213+
'/datahub/$subjectId/$recordId': typeof AppDatahubSubjectIdRecordIdRoute
205214
'/datahub/$subjectId/assignments': typeof AppDatahubSubjectIdAssignmentsRoute
206215
'/datahub/$subjectId/graph': typeof AppDatahubSubjectIdGraphRoute
207216
'/datahub/$subjectId/table': typeof AppDatahubSubjectIdTableRoute
@@ -229,6 +238,7 @@ export interface FileRoutesById {
229238
'/_app/upload/': typeof AppUploadIndexRoute
230239
'/_app/admin/groups/create': typeof AppAdminGroupsCreateRoute
231240
'/_app/admin/users/create': typeof AppAdminUsersCreateRoute
241+
'/_app/datahub/$subjectId/$recordId': typeof AppDatahubSubjectIdRecordIdRoute
232242
'/_app/datahub/$subjectId/assignments': typeof AppDatahubSubjectIdAssignmentsRoute
233243
'/_app/datahub/$subjectId/graph': typeof AppDatahubSubjectIdGraphRoute
234244
'/_app/datahub/$subjectId/table': typeof AppDatahubSubjectIdTableRoute
@@ -256,6 +266,7 @@ export interface FileRouteTypes {
256266
| '/upload'
257267
| '/admin/groups/create'
258268
| '/admin/users/create'
269+
| '/datahub/$subjectId/$recordId'
259270
| '/datahub/$subjectId/assignments'
260271
| '/datahub/$subjectId/graph'
261272
| '/datahub/$subjectId/table'
@@ -281,6 +292,7 @@ export interface FileRouteTypes {
281292
| '/upload'
282293
| '/admin/groups/create'
283294
| '/admin/users/create'
295+
| '/datahub/$subjectId/$recordId'
284296
| '/datahub/$subjectId/assignments'
285297
| '/datahub/$subjectId/graph'
286298
| '/datahub/$subjectId/table'
@@ -307,6 +319,7 @@ export interface FileRouteTypes {
307319
| '/_app/upload/'
308320
| '/_app/admin/groups/create'
309321
| '/_app/admin/users/create'
322+
| '/_app/datahub/$subjectId/$recordId'
310323
| '/_app/datahub/$subjectId/assignments'
311324
| '/_app/datahub/$subjectId/graph'
312325
| '/_app/datahub/$subjectId/table'
@@ -477,6 +490,13 @@ declare module '@tanstack/react-router' {
477490
preLoaderRoute: typeof AppDatahubSubjectIdAssignmentsRouteImport
478491
parentRoute: typeof AppDatahubSubjectIdRouteRoute
479492
}
493+
'/_app/datahub/$subjectId/$recordId': {
494+
id: '/_app/datahub/$subjectId/$recordId'
495+
path: '/$recordId'
496+
fullPath: '/datahub/$subjectId/$recordId'
497+
preLoaderRoute: typeof AppDatahubSubjectIdRecordIdRouteImport
498+
parentRoute: typeof AppDatahubSubjectIdRouteRoute
499+
}
480500
'/_app/admin/users/create': {
481501
id: '/_app/admin/users/create'
482502
path: '/admin/users/create'
@@ -495,13 +515,15 @@ declare module '@tanstack/react-router' {
495515
}
496516

497517
interface AppDatahubSubjectIdRouteRouteChildren {
518+
AppDatahubSubjectIdRecordIdRoute: typeof AppDatahubSubjectIdRecordIdRoute
498519
AppDatahubSubjectIdAssignmentsRoute: typeof AppDatahubSubjectIdAssignmentsRoute
499520
AppDatahubSubjectIdGraphRoute: typeof AppDatahubSubjectIdGraphRoute
500521
AppDatahubSubjectIdTableRoute: typeof AppDatahubSubjectIdTableRoute
501522
}
502523

503524
const AppDatahubSubjectIdRouteRouteChildren: AppDatahubSubjectIdRouteRouteChildren =
504525
{
526+
AppDatahubSubjectIdRecordIdRoute: AppDatahubSubjectIdRecordIdRoute,
505527
AppDatahubSubjectIdAssignmentsRoute: AppDatahubSubjectIdAssignmentsRoute,
506528
AppDatahubSubjectIdGraphRoute: AppDatahubSubjectIdGraphRoute,
507529
AppDatahubSubjectIdTableRoute: AppDatahubSubjectIdTableRoute,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { InstrumentSummary } from '@opendatacapture/react-core';
2+
import { createFileRoute } from '@tanstack/react-router';
3+
4+
import { useInstrument } from '@/hooks/useInstrument';
5+
import { instrumentRecordQueryOptions, useInstrumentRecordQuery } from '@/hooks/useInstrumentRecordQuery';
6+
import { subjectQueryOptions, useSubjectQuery } from '@/hooks/useSubjectQuery';
7+
8+
const RouteComponent = () => {
9+
const recordId = Route.useParams({ select: (params) => params.recordId });
10+
11+
const { data: instrumentRecord } = useInstrumentRecordQuery({ params: { id: recordId } });
12+
const { data: subject } = useSubjectQuery({ params: { id: instrumentRecord.subjectId } });
13+
14+
const instrument = useInstrument(instrumentRecord.instrumentId);
15+
16+
if (!instrument) {
17+
return null;
18+
}
19+
20+
return (
21+
<div className="container py-8">
22+
<InstrumentSummary
23+
displayAllMeasures
24+
data={instrumentRecord.data}
25+
instrument={instrument}
26+
subject={subject}
27+
timeCollected={instrumentRecord.createdAt.getTime()}
28+
/>
29+
</div>
30+
);
31+
};
32+
33+
export const Route = createFileRoute('/_app/datahub/$subjectId/$recordId')({
34+
component: RouteComponent,
35+
loader: async ({ context, params }) => {
36+
const record = await context.queryClient.ensureQueryData(
37+
instrumentRecordQueryOptions({ params: { id: params.recordId } })
38+
);
39+
await context.queryClient.ensureQueryData(subjectQueryOptions({ params: { id: record.subjectId } }));
40+
}
41+
});

apps/web/src/routes/_app/datahub/$subjectId/assignments.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { UnilingualInstrumentInfo } from '@opendatacapture/schemas/instrume
2121
import { createFileRoute } from '@tanstack/react-router';
2222
import { z } from 'zod/v4';
2323

24+
import { QRCode } from '@/components/QRCode';
2425
import { useAssignmentsQuery } from '@/hooks/useAssignmentsQuery';
2526
import { useCreateAssignment } from '@/hooks/useCreateAssignment';
2627
import { useInstrument } from '@/hooks/useInstrument';
@@ -40,26 +41,25 @@ const AssignmentSlider: React.FC<{
4041
const instrument = useInstrument(assignment?.instrumentId ?? null);
4142

4243
return (
43-
<Sheet open={isOpen} onOpenChange={setIsOpen}>
44+
<Sheet open={Boolean(isOpen && assignment && instrument)} onOpenChange={setIsOpen}>
4445
<Sheet.Content className="flex h-full flex-col">
4546
<Sheet.Header>
4647
<Sheet.Title>{instrument?.details.title}</Sheet.Title>
4748
<Sheet.Description>{t('datahub.assignments.assignmentSliderDesc')}</Sheet.Description>
4849
</Sheet.Header>
4950
<Sheet.Body className="grow">
50-
{instrument && (
51-
<div className="flex flex-col gap-3">
52-
<Label asChild>
53-
<a className="hover:underline" href={assignment!.url} rel="noreferrer" target="_blank">
54-
{t('datahub.assignments.link')}
55-
</a>
56-
</Label>
57-
<div className="flex gap-2">
58-
<Input readOnly className="h-9" id="link" value={assignment!.url} />
59-
<CopyButton size="sm" text={assignment!.url} variant="outline" />
60-
</div>
51+
<div className="flex flex-col gap-3">
52+
<Label asChild>
53+
<a className="hover:underline" href={assignment?.url} rel="noreferrer" target="_blank">
54+
{t('datahub.assignments.link')}
55+
</a>
56+
</Label>
57+
<div className="flex gap-2">
58+
<Input readOnly className="h-9" id="link" value={assignment?.url} />
59+
<CopyButton size="sm" text={assignment?.url ?? ''} variant="outline" />
6160
</div>
62-
)}
61+
<QRCode url={assignment?.url ?? 'javascript:void(0)'} />
62+
</div>
6363
</Sheet.Body>
6464
<Sheet.Footer>
6565
<Button

0 commit comments

Comments
 (0)