Skip to content

Commit a51e371

Browse files
committed
feat: introduce questionnaire start form for including validations
1 parent 40759d1 commit a51e371

File tree

6 files changed

+72
-7
lines changed

6 files changed

+72
-7
lines changed

apps/backend/mikro-orm.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Migrator } from "@mikro-orm/migrations";
33
import { TsMorphMetadataProvider } from "@mikro-orm/reflection";
44
import { SeedManager } from "@mikro-orm/seeder";
55
import { configuration } from "./src/config/configuration";
6+
import { HttpException, HttpStatus } from "@nestjs/common";
67

78
const c = configuration();
89

@@ -17,6 +18,7 @@ export default defineConfig({
1718
driver: PostgreSqlDriver,
1819
metadataProvider: TsMorphMetadataProvider,
1920
extensions: [Migrator, SeedManager],
21+
findOneOrFailHandler: (entityName) => new HttpException(`${entityName} not found.`, HttpStatus.NOT_FOUND),
2022
migrations: {
2123
path: "./db/migrations",
2224
pathTs: "./db/migrations",

apps/backend/src/research/participants/participants.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Body, Controller, Delete, Get, Param, Patch, Post } from "@nestjs/common";
22
import { ParticipantsService } from "./participants.service";
3-
import { ApiOperation, ApiTags, ApiUnprocessableEntityResponse } from "@nestjs/swagger";
3+
import { ApiNotFoundResponse, ApiOperation, ApiTags, ApiUnprocessableEntityResponse } from "@nestjs/swagger";
44
import { ParticipantCreationDto, ParticipantMutationDto, ParticipantResponseDto } from "./participant.dto";
55
import { ErrorResponseDto } from "../../common/dto/error.dto";
66
import { Roles } from "../../system/users/roles.decorator";
@@ -26,6 +26,7 @@ export class ParticipantsController {
2626

2727
@Get(":id")
2828
@ApiOperation({ summary: "Get a participant by ID" })
29+
@ApiNotFoundResponse({ description: "Entity not found exception", type: ErrorResponseDto })
2930
get(@Param("id") id: string): Promise<ParticipantResponseDto> {
3031
return this.participantService.findOne(+id);
3132
}

apps/backend/src/research/studies/studies.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Body, Controller, Delete, Get, Param, Patch, Post } from "@nestjs/common";
2-
import { ApiOperation, ApiTags, ApiUnprocessableEntityResponse } from "@nestjs/swagger";
2+
import { ApiNotFoundResponse, ApiOperation, ApiTags, ApiUnprocessableEntityResponse } from "@nestjs/swagger";
33
import { ErrorResponseDto } from "../../common/dto/error.dto";
44
import { Roles } from "../../system/users/roles.decorator";
55
import { UserRole } from "../../system/users/user.entity";
@@ -26,6 +26,7 @@ export class StudiesController {
2626

2727
@Get(":id")
2828
@ApiOperation({ summary: "Get a study by ID" })
29+
@ApiNotFoundResponse({ description: "Entity not found exception", type: ErrorResponseDto })
2930
get(@Param("id") id: string): Promise<StudyResponseDto> {
3031
return this.studiesService.findOne(+id);
3132
}

apps/frontend/src/api.gen.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,15 @@ export interface operations {
15741574
"application/json": components["schemas"]["ParticipantResponseDto"];
15751575
};
15761576
};
1577+
/** @description Entity not found exception */
1578+
404: {
1579+
headers: {
1580+
[name: string]: unknown;
1581+
};
1582+
content: {
1583+
"application/json": components["schemas"]["ErrorResponseDto"];
1584+
};
1585+
};
15771586
};
15781587
};
15791588
ParticipantsController_delete: {
@@ -2038,6 +2047,15 @@ export interface operations {
20382047
"application/json": components["schemas"]["StudyResponseDto"];
20392048
};
20402049
};
2050+
/** @description Entity not found exception */
2051+
404: {
2052+
headers: {
2053+
[name: string]: unknown;
2054+
};
2055+
content: {
2056+
"application/json": components["schemas"]["ErrorResponseDto"];
2057+
};
2058+
};
20412059
};
20422060
};
20432061
StudiesController_delete: {

apps/frontend/src/routes/_auth/questionnaire/_form/index.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,61 @@ import { Button, Stack, TextInput } from "@quassel/ui";
22
import { createFileRoute, useNavigate } from "@tanstack/react-router";
33
import { i18n } from "../../../../stores/i18n";
44
import { useStore } from "@nanostores/react";
5+
import { useForm } from "@mantine/form";
6+
import { $api } from "../../../../stores/api";
7+
import { $questionnaire } from "../../../../stores/questionnaire";
8+
9+
type FormValues = {
10+
participantId: string;
11+
studyId: string;
12+
};
513

614
export const messages = i18n("questionnaire", {
715
title: "Start new questionnaire",
16+
participantIdLabel: "Child ID",
17+
studyIdLabel: "Study ID",
818
formAction: "Continue",
919
});
1020

1121
function Questionnaire() {
1222
const n = useNavigate();
23+
1324
const t = useStore(messages);
1425

15-
const handleSubmit = () => {
16-
n({ to: "/questionnaire/participant" });
26+
const f = useForm<FormValues>();
27+
28+
const validateParticipantMutation = $api.useMutation("get", "/participants/{id}", {
29+
onError(error) {
30+
if (error.statusCode === 404) {
31+
f.setFieldError("participantId", "Participant not found.");
32+
}
33+
},
34+
});
35+
const validateStudyMutation = $api.useMutation("get", "/studies/{id}", {
36+
onError(error) {
37+
if (error.statusCode === 404) {
38+
f.setFieldError("studyId", "Study not found.");
39+
}
40+
},
41+
});
42+
43+
const handleSubmit = async (values: FormValues) => {
44+
const participantCheck = validateParticipantMutation.mutateAsync({ params: { path: { id: values.participantId } } });
45+
const studyCheck = validateStudyMutation.mutateAsync({ params: { path: { id: values.studyId } } });
46+
47+
Promise.all([participantCheck, studyCheck]).then(([participant, study]) => {
48+
$questionnaire.set({ participant, study });
49+
n({ to: "/questionnaire/participant" });
50+
});
1751
};
1852

1953
return (
2054
<>
2155
<h3>{t.title}</h3>
22-
<form onSubmit={handleSubmit}>
56+
<form onSubmit={f.onSubmit(handleSubmit)}>
2357
<Stack>
24-
<TextInput />
25-
<TextInput />
58+
<TextInput required label={t.participantIdLabel} {...f.getInputProps("participantId")} />
59+
<TextInput required label={t.studyIdLabel} {...f.getInputProps("studyId")} />
2660
<Button type="submit">{t.formAction}</Button>
2761
</Stack>
2862
</form>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { components } from "../api.gen";
2+
import { map } from "nanostores";
3+
4+
type Questionnaire = {
5+
participant: components["schemas"]["ParticipantResponseDto"];
6+
study: components["schemas"]["StudyResponseDto"];
7+
};
8+
9+
export const $questionnaire = map<Questionnaire>();

0 commit comments

Comments
 (0)