Skip to content

Commit 5735bcb

Browse files
authored
Merge pull request #222 from openscript-ch/26-implement-questionnaire-viewing-and-editing
26 implement questionnaire viewing and editing
2 parents e3060da + 5e1bebe commit 5735bcb

File tree

12 files changed

+214
-147
lines changed

12 files changed

+214
-147
lines changed

.changeset/fast-monkeys-cheat.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@quassel/frontend": patch
3+
"@quassel/backend": patch
4+
---
5+
6+
Show more infos for questionnaires in admin

.changeset/sour-ducks-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@quassel/frontend": patch
3+
---
4+
5+
Make questionnaires editable from admin

.changeset/spotty-news-draw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@quassel/frontend": patch
3+
---
4+
5+
Fix preselecting start date when creating next period

apps/backend/src/research/questionnaires/questionnaires.service.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,19 @@ export class QuestionnairesService {
5151
throw e;
5252
}
5353

54-
return (await questionnaire.populate(["entries", "entries.carer", "entries.entryLanguages.language", "participant"])).toObject();
54+
return (await questionnaire.populate(["entries", "entries.carer", "entries.entryLanguages.language", "participant", "study"])).toObject();
5555
}
5656

5757
async findAll() {
58-
return (await this.questionnaireRepository.findAll()).map((questionnaire) => questionnaire.toObject());
58+
return (await this.questionnaireRepository.findAll({ populate: ["study", "participant"] })).map((questionnaire) =>
59+
questionnaire.toObject()
60+
);
5961
}
6062

6163
async findOne(id: number) {
6264
return (
6365
await this.questionnaireRepository.findOneOrFail(id, {
64-
populate: ["entries", "entries.carer", "entries.entryLanguages.language", "participant"],
66+
populate: ["entries", "entries.carer", "entries.entryLanguages.language", "participant", "study"],
6567
})
6668
).toObject();
6769
}
@@ -76,11 +78,16 @@ export class QuestionnairesService {
7678

7779
async update(id: number, questionnaireMutationDto: QuestionnaireMutationDto) {
7880
const questionnaire = await this.questionnaireRepository.findOneOrFail(id, {
79-
populate: ["entries", "entries.carer", "entries.entryLanguages.language", "participant"],
81+
populate: ["entries", "entries.carer", "entries.entryLanguages.language", "participant", "study"],
8082
});
83+
84+
const prevQuestionnaire = await this.questionnaireRepository.findOne(
85+
{ participant: questionnaire.participant, endedAt: { $lt: questionnaire.startedAt } },
86+
{ orderBy: { endedAt: "desc" } }
87+
);
88+
8189
questionnaire.assign(questionnaireMutationDto);
8290

83-
const prevQuestionnaire = await this.findLatestByParticipant(questionnaire.participant!.id);
8491
if (prevQuestionnaire?.id !== id) {
8592
this.validateStartDate(questionnaire, prevQuestionnaire);
8693
}

apps/frontend/src/api.gen.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -296,23 +296,6 @@ export interface paths {
296296
patch: operations["QuestionnairesController_update"];
297297
trace?: never;
298298
};
299-
"/questionnaires/{id}/complete": {
300-
parameters: {
301-
query?: never;
302-
header?: never;
303-
path?: never;
304-
cookie?: never;
305-
};
306-
get?: never;
307-
put?: never;
308-
post?: never;
309-
delete?: never;
310-
options?: never;
311-
head?: never;
312-
/** Completes the questionnaire by ID */
313-
patch: operations["QuestionnairesController_complete"];
314-
trace?: never;
315-
};
316299
"/entry-languages": {
317300
parameters: {
318301
query?: never;
@@ -2003,27 +1986,6 @@ export interface operations {
20031986
};
20041987
};
20051988
};
2006-
QuestionnairesController_complete: {
2007-
parameters: {
2008-
query?: never;
2009-
header?: never;
2010-
path: {
2011-
id: string;
2012-
};
2013-
cookie?: never;
2014-
};
2015-
requestBody?: never;
2016-
responses: {
2017-
200: {
2018-
headers: {
2019-
[name: string]: unknown;
2020-
};
2021-
content: {
2022-
"application/json": Record<string, never>;
2023-
};
2024-
};
2025-
};
2026-
};
20271989
EntryLanguagesController_index: {
20281990
parameters: {
20291991
query?: never;

apps/frontend/src/components/questionnaire/PeriodForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function PeriodForm({ onSave, actionLabel, period, startDate }: PeriodFor
5353
}, [period]);
5454

5555
useEffect(() => {
56-
if (startDate) f.reset();
56+
if (startDate) f.setValues({ range: [startDate, null] });
5757
}, [startDate]);
5858

5959
return (
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { notifications } from "@quassel/ui";
2+
import { $api } from "../../stores/api";
3+
import { EntryCalendar } from "./calendar/EntryCalendar";
4+
import { useQueryClient } from "@tanstack/react-query";
5+
import { EntryFormValues } from "./calendar/EntryForm";
6+
import { components } from "../../api.gen";
7+
import { GapsPerDay } from "../../utils/entry";
8+
import { i18n } from "../../stores/i18n";
9+
import { useStore } from "@nanostores/react";
10+
11+
type QuestionnaireEntriesProps = {
12+
questionnaire: components["schemas"]["QuestionnaireResponseDto"];
13+
gaps?: GapsPerDay;
14+
};
15+
16+
const messages = i18n("questionnaireEntries", {
17+
notificationSuccessCreateLanguage: "Successfully add a new language.",
18+
notificationSuccessCreateCarer: "Successfully add a new carer.",
19+
});
20+
21+
export function QuestionnaireEntries({ questionnaire, gaps }: QuestionnaireEntriesProps) {
22+
const c = useQueryClient();
23+
const t = useStore(messages);
24+
25+
const participantId = questionnaire.participant?.id;
26+
27+
const createMutation = $api.useMutation("post", "/entries");
28+
const updateMutation = $api.useMutation("patch", "/entries/{id}");
29+
const deleteMutation = $api.useMutation("delete", "/entries/{id}");
30+
31+
const { data: languages } = $api.useQuery("get", "/languages", { params: { query: { participantId } } });
32+
const createLanguageMutation = $api.useMutation("post", "/languages", {
33+
onSuccess() {
34+
notifications.show({ message: t.notificationSuccessCreateLanguage, color: "uzhGreen" });
35+
c.refetchQueries($api.queryOptions("get", "/languages", { params: { query: { participantId } } }));
36+
},
37+
});
38+
39+
const { data: carers } = $api.useQuery("get", "/carers", { params: { query: { participantId } } });
40+
const createCarerMutation = $api.useMutation("post", "/carers", {
41+
onSuccess() {
42+
notifications.show({ message: t.notificationSuccessCreateCarer, color: "uzhGreen" });
43+
c.refetchQueries($api.queryOptions("get", "/carers", { params: { query: { participantId } } }));
44+
},
45+
});
46+
47+
const reloadEntries = () => {
48+
c.invalidateQueries($api.queryOptions("get", "/questionnaires/{id}", { params: { path: { id: questionnaire.id.toString() } } }));
49+
};
50+
51+
const handleCreate = ({ carer, ...rest }: EntryFormValues, weekday: number) => {
52+
const entryRequest = {
53+
...rest,
54+
carer: carer!,
55+
weekday,
56+
questionnaire: questionnaire.id,
57+
};
58+
59+
return createMutation.mutateAsync({ body: entryRequest }, { onSuccess: reloadEntries });
60+
};
61+
62+
const handleUpdate = (id: number, { carer, ...rest }: Partial<EntryFormValues>, weekday: number) => {
63+
const entryRequest = {
64+
...rest,
65+
carer: carer!,
66+
weekday,
67+
questionnaire: questionnaire.id,
68+
};
69+
70+
return updateMutation.mutateAsync({ body: entryRequest, params: { path: { id: id.toString() } } }, { onSuccess: reloadEntries });
71+
};
72+
73+
const handleDelete = (id: number) => {
74+
return deleteMutation.mutateAsync({ params: { path: { id: id.toString() } } }, { onSuccess: reloadEntries });
75+
};
76+
77+
return (
78+
<EntryCalendar
79+
entries={questionnaire.entries ?? []}
80+
gaps={gaps}
81+
onAddEntry={handleCreate}
82+
onUpdateEntry={handleUpdate}
83+
onDeleteEntry={handleDelete}
84+
carers={carers ?? []}
85+
languages={languages ?? []}
86+
onAddCarer={(name) => createCarerMutation.mutateAsync({ body: { name, participant: participantId } }).then(({ id }) => id)}
87+
onAddLanguage={(name) => createLanguageMutation.mutateAsync({ body: { name, participant: participantId } }).then(({ id }) => id)}
88+
/>
89+
);
90+
}

apps/frontend/src/routes/_auth/administration/questionnaires/edit.$id.tsx

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,42 @@
11
import { createFileRoute, useNavigate } from "@tanstack/react-router";
2-
import { components } from "../../../../api.gen";
32
import { $api } from "../../../../stores/api";
43
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
5-
import { Button, TextInput, useForm } from "@quassel/ui";
4+
import { Button, DateInput, Divider, Group, Stack, Table, Textarea, TextInput, useForm } from "@quassel/ui";
65
import { useEffect } from "react";
6+
import { QuestionnaireEntries } from "../../../../components/questionnaire/QuestionnaireEntries";
7+
import { format, i18n } from "../../../../stores/i18n";
8+
import { useStore } from "@nanostores/react";
79

8-
type FormValues = components["schemas"]["QuestionnaireMutationDto"];
10+
type FormValues = {
11+
startedAt: Date;
12+
endedAt: Date;
13+
title: string;
14+
remark?: string;
15+
};
16+
17+
const messages = i18n("questionnairesEdit", {
18+
labelRemarks: "Remarks",
19+
labelTitle: "Title",
20+
labelEndedAt: "End date",
21+
labelStartedAt: "Start date",
22+
labelParticipant: "Participant",
23+
labelStudy: "Study",
24+
});
925

1026
function AdministrationQuestionnairesEdit() {
1127
const p = Route.useParams();
28+
const n = useNavigate();
1229
const q = useQueryClient();
30+
31+
const t = useStore(messages);
32+
const { time } = useStore(format);
33+
1334
const { data, isSuccess } = useSuspenseQuery(
1435
$api.queryOptions("get", "/questionnaires/{id}", {
1536
params: { path: { id: p.id } },
1637
})
1738
);
18-
const n = useNavigate();
39+
1940
const editQuestionnaireMutation = $api.useMutation("patch", "/questionnaires/{id}", {
2041
onSuccess: () => {
2142
q.invalidateQueries(
@@ -26,27 +47,55 @@ function AdministrationQuestionnairesEdit() {
2647
n({ to: "/administration/questionnaires" });
2748
},
2849
});
50+
2951
const f = useForm<FormValues>();
30-
const handleSubmit = (values: FormValues) => {
52+
const handleSubmit = ({ startedAt, endedAt, ...rest }: FormValues) => {
3153
editQuestionnaireMutation.mutate({
32-
body: { ...values },
54+
body: { ...rest, startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString() },
3355
params: { path: { id: p.id } },
3456
});
3557
};
3658

3759
useEffect(() => {
38-
f.setValues({ title: data.title });
39-
f.resetDirty();
60+
const { title, remark, startedAt, endedAt } = data;
61+
f.initialize({ title, remark, startedAt: new Date(startedAt), endedAt: new Date(endedAt) });
4062
}, [isSuccess, data]);
4163

4264
return (
43-
<form autoComplete="off" onSubmit={f.onSubmit(handleSubmit)}>
44-
<TextInput label="Name" type="name" {...f.getInputProps("title")} />
65+
<Stack gap="xl">
66+
<Table>
67+
<Table.Tbody>
68+
<Table.Tr>
69+
<Table.Th>{t.labelParticipant}</Table.Th>
70+
<Table.Td>{data.participant.id}</Table.Td>
71+
<Table.Td>{data.participant.birthday && time(new Date(data.participant.birthday))}</Table.Td>
72+
</Table.Tr>
73+
<Table.Tr>
74+
<Table.Th>{t.labelStudy}</Table.Th>
75+
<Table.Td>{data?.study.id}</Table.Td>
76+
<Table.Td>{data?.study.title}</Table.Td>
77+
</Table.Tr>
78+
</Table.Tbody>
79+
</Table>
80+
81+
<form autoComplete="off" onSubmit={f.onSubmit(handleSubmit)}>
82+
<Stack>
83+
<TextInput {...f.getInputProps("title")} label={t.labelTitle} />
84+
<Group>
85+
<DateInput {...f.getInputProps("startedAt")} label={t.labelStartedAt} flex={1} />
86+
<DateInput {...f.getInputProps("endedAt")} label={t.labelEndedAt} flex={1} />
87+
</Group>
88+
89+
<Textarea {...f.getInputProps("remark")} label={t.labelRemarks} rows={8} />
4590

46-
<Button type="submit" fullWidth mt="xl" loading={editQuestionnaireMutation.isPending}>
47-
Change
48-
</Button>
49-
</form>
91+
<Button type="submit" loading={editQuestionnaireMutation.isPending}>
92+
Change
93+
</Button>
94+
</Stack>
95+
</form>
96+
<Divider />
97+
<QuestionnaireEntries questionnaire={data} />
98+
</Stack>
5099
);
51100
}
52101

apps/frontend/src/routes/_auth/administration/questionnaires/index.tsx

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createFileRoute, Link } from "@tanstack/react-router";
22
import { $api } from "../../../../stores/api";
3-
import { Button, Table } from "@quassel/ui";
3+
import { Button, Group, Table } from "@quassel/ui";
44
import { $session } from "../../../../stores/session";
55
import { useStore } from "@nanostores/react";
66

@@ -16,29 +16,36 @@ function AdministrationQuestionnairesIndex() {
1616
<Table.Thead>
1717
<Table.Tr>
1818
<Table.Th>Id</Table.Th>
19-
<Table.Th>Name</Table.Th>
19+
<Table.Th>Child</Table.Th>
20+
<Table.Th>Title</Table.Th>
21+
<Table.Th>Study</Table.Th>
2022
</Table.Tr>
2123
</Table.Thead>
2224
<Table.Tbody>
2325
{data?.map((q) => (
2426
<Table.Tr key={q.id}>
2527
<Table.Td>{q.id}</Table.Td>
28+
<Table.Td>{q.participant.id}</Table.Td>
29+
<Table.Td>{q.title}</Table.Td>
30+
<Table.Td>{q.study.title}</Table.Td>
2631
<Table.Td>
27-
<Button variant="default" renderRoot={(props) => <Link to={`/administration/questionnaires/edit/${q.id}`} {...props} />}>
28-
Edit
29-
</Button>
30-
{sessionStore.role === "ADMIN" && (
31-
<Button
32-
variant="default"
33-
onClick={() =>
34-
deleteQuestionnaireMutation.mutate({
35-
params: { path: { id: q.id.toString() } },
36-
})
37-
}
38-
>
39-
Delete
32+
<Group>
33+
<Button variant="default" renderRoot={(props) => <Link to={`/administration/questionnaires/edit/${q.id}`} {...props} />}>
34+
Edit
4035
</Button>
41-
)}
36+
{sessionStore.role === "ADMIN" && (
37+
<Button
38+
variant="default"
39+
onClick={() =>
40+
deleteQuestionnaireMutation.mutate({
41+
params: { path: { id: q.id.toString() } },
42+
})
43+
}
44+
>
45+
Delete
46+
</Button>
47+
)}
48+
</Group>
4249
</Table.Td>
4350
</Table.Tr>
4451
))}

0 commit comments

Comments
 (0)