Skip to content

Commit 5c5eb81

Browse files
committed
feat: preselect start date based on previous questionnaire
1 parent fb9d75d commit 5c5eb81

File tree

10 files changed

+101
-63
lines changed

10 files changed

+101
-63
lines changed

apps/backend/src/research/participants/participant.dto.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ApiProperty, OmitType, PartialType } from "@nestjs/swagger";
22
import { Type } from "class-transformer";
33
import { IsDateString, IsOptional } from "class-validator";
4+
import { QuestionnaireListResponseDto } from "../questionnaires/questionnaire.dto";
45

56
export class ParticipantDto {
67
@ApiProperty({ example: 1, description: "The id of the participant (child id)" })
@@ -11,6 +12,9 @@ export class ParticipantDto {
1112
@IsOptional()
1213
birthday?: Date;
1314

15+
@Type(() => QuestionnaireListResponseDto)
16+
latestQuestionnaire?: QuestionnaireListResponseDto;
17+
1418
@Type(() => Array<number>)
1519
questionnaires: number[];
1620

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ import { ParticipantCreationDto, ParticipantMutationDto, ParticipantResponseDto
55
import { ErrorResponseDto } from "../../common/dto/error.dto";
66
import { Roles } from "../../system/users/roles.decorator";
77
import { UserRole } from "../../system/users/user.entity";
8+
import { QuestionnairesService } from "../questionnaires/questionnaires.service";
89

910
@ApiTags("Participants")
1011
@Controller("participants")
1112
export class ParticipantsController {
12-
constructor(private readonly participantService: ParticipantsService) {}
13+
constructor(
14+
private readonly participantService: ParticipantsService,
15+
private readonly questionnairesService: QuestionnairesService
16+
) {}
1317

1418
@Post()
1519
@ApiOperation({ summary: "Create a participant" })
@@ -27,8 +31,10 @@ export class ParticipantsController {
2731
@Get(":id")
2832
@ApiOperation({ summary: "Get a participant by ID" })
2933
@ApiNotFoundResponse({ description: "Entity not found exception", type: ErrorResponseDto })
30-
get(@Param("id") id: string): Promise<ParticipantResponseDto> {
31-
return this.participantService.findOne(+id);
34+
async get(@Param("id") id: string): Promise<ParticipantResponseDto> {
35+
const participant = await this.participantService.findOne(+id);
36+
const latestQuestionnaire = (await this.questionnairesService.findLatestByParticipant(+id))?.toObject();
37+
return { ...participant, latestQuestionnaire };
3238
}
3339

3440
@Patch(":id")

apps/backend/src/research/questionnaires/questionnaire.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class QuestionnaireDto {
3535
}
3636
export class QuestionnaireResponseDto extends QuestionnaireDto {}
3737
export class EntryQuestionnaireDto extends OmitType(QuestionnaireDto, ["entries"]) {}
38-
export class QuestionnairesResponseDto extends OmitType(QuestionnaireDto, ["entries"]) {}
38+
export class QuestionnaireListResponseDto extends OmitType(QuestionnaireDto, ["entries"]) {}
3939
export class QuestionnaireCreationDto extends OmitType(QuestionnaireDto, ["id", "study", "participant"]) {
4040
study: number;
4141
participant: number;

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { ErrorResponseDto } from "../../common/dto/error.dto";
44
import { Roles } from "../../system/users/roles.decorator";
55
import { UserRole } from "../../system/users/user.entity";
66
import { QuestionnairesService } from "./questionnaires.service";
7-
import { QuestionnaireCreationDto, QuestionnaireResponseDto, QuestionnaireMutationDto, QuestionnairesResponseDto } from "./questionnaire.dto";
7+
import {
8+
QuestionnaireCreationDto,
9+
QuestionnaireResponseDto,
10+
QuestionnaireMutationDto,
11+
QuestionnaireListResponseDto,
12+
} from "./questionnaire.dto";
813

914
@ApiTags("Questionnaires")
1015
@Controller("questionnaires")
@@ -20,7 +25,7 @@ export class QuestionnairesController {
2025

2126
@Get()
2227
@ApiOperation({ summary: "Get all questionnairess" })
23-
index(): Promise<QuestionnairesResponseDto[]> {
28+
index(): Promise<QuestionnaireListResponseDto[]> {
2429
return this.questionnairesService.findAll();
2530
}
2631

apps/frontend/src/api.gen.ts

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,50 @@ export interface components {
471471
*/
472472
role?: "ASSISTANT" | "ADMIN";
473473
};
474+
StudyDto: {
475+
/**
476+
* @description The id of the study (child id)
477+
* @example 1
478+
*/
479+
id: number;
480+
/**
481+
* @description The title of the study
482+
* @example Series 1
483+
*/
484+
title: string;
485+
questionnaires?: number[];
486+
};
487+
QuestionnaireListResponseDto: {
488+
/**
489+
* @description The id of the questionnaire
490+
* @example 1
491+
*/
492+
id: number;
493+
/**
494+
* Format: date-time
495+
* @description The starting date of the questionnaire
496+
* @example 2024-11-01T07:00:00.000Z
497+
*/
498+
startedAt?: string;
499+
/**
500+
* Format: date-time
501+
* @description The ending date of the questionnaire
502+
* @example 2024-11-01T08:00:00.00Z
503+
*/
504+
endedAt?: string;
505+
/**
506+
* @description The title of the questionnaire
507+
* @example First few months
508+
*/
509+
title?: string;
510+
/**
511+
* @description The remark of the questionnaire
512+
* @example We went on holidays for 2 weeks and only spoke Esperanto
513+
*/
514+
remark?: string;
515+
study?: components["schemas"]["StudyDto"];
516+
participant?: components["schemas"]["ParticipantDto"];
517+
};
474518
ParticipantDto: {
475519
/**
476520
* @description The id of the participant (child id)
@@ -483,6 +527,7 @@ export interface components {
483527
* @example 2024-11-01T00:05:02.718Z
484528
*/
485529
birthday?: string;
530+
latestQuestionnaire?: components["schemas"]["QuestionnaireListResponseDto"];
486531
questionnaires: number[];
487532
carers: number[];
488533
languages: number[];
@@ -574,6 +619,7 @@ export interface components {
574619
* @example 2024-11-01T00:05:02.718Z
575620
*/
576621
birthday?: string;
622+
latestQuestionnaire?: components["schemas"]["QuestionnaireListResponseDto"];
577623
};
578624
ParticipantResponseDto: {
579625
/**
@@ -587,6 +633,7 @@ export interface components {
587633
* @example 2024-11-01T00:05:02.718Z
588634
*/
589635
birthday?: string;
636+
latestQuestionnaire?: components["schemas"]["QuestionnaireListResponseDto"];
590637
questionnaires: number[];
591638
carers: number[];
592639
languages: number[];
@@ -603,6 +650,7 @@ export interface components {
603650
* @example 2024-11-01T00:05:02.718Z
604651
*/
605652
birthday?: string;
653+
latestQuestionnaire?: components["schemas"]["QuestionnaireListResponseDto"];
606654
questionnaires?: number[];
607655
carers?: number[];
608656
languages?: number[];
@@ -640,19 +688,6 @@ export interface components {
640688
questionnaire: number;
641689
entryLanguages: components["schemas"]["EntryLanguageCreationDto"][];
642690
};
643-
StudyDto: {
644-
/**
645-
* @description The id of the study (child id)
646-
* @example 1
647-
*/
648-
id: number;
649-
/**
650-
* @description The title of the study
651-
* @example Series 1
652-
*/
653-
title: string;
654-
questionnaires?: number[];
655-
};
656691
EntryQuestionnaireDto: {
657692
/**
658693
* @description The id of the questionnaire
@@ -873,37 +908,6 @@ export interface components {
873908
participant?: components["schemas"]["ParticipantDto"];
874909
entries?: components["schemas"]["QuestionnaireEntryDto"][];
875910
};
876-
QuestionnairesResponseDto: {
877-
/**
878-
* @description The id of the questionnaire
879-
* @example 1
880-
*/
881-
id: number;
882-
/**
883-
* Format: date-time
884-
* @description The starting date of the questionnaire
885-
* @example 2024-11-01T07:00:00.000Z
886-
*/
887-
startedAt?: string;
888-
/**
889-
* Format: date-time
890-
* @description The ending date of the questionnaire
891-
* @example 2024-11-01T08:00:00.00Z
892-
*/
893-
endedAt?: string;
894-
/**
895-
* @description The title of the questionnaire
896-
* @example First few months
897-
*/
898-
title?: string;
899-
/**
900-
* @description The remark of the questionnaire
901-
* @example We went on holidays for 2 weeks and only spoke Esperanto
902-
*/
903-
remark?: string;
904-
study?: components["schemas"]["StudyDto"];
905-
participant?: components["schemas"]["ParticipantDto"];
906-
};
907911
QuestionnaireMutationDto: {
908912
/**
909913
* Format: date-time
@@ -1827,7 +1831,7 @@ export interface operations {
18271831
[name: string]: unknown;
18281832
};
18291833
content: {
1830-
"application/json": components["schemas"]["QuestionnairesResponseDto"][];
1834+
"application/json": components["schemas"]["QuestionnaireListResponseDto"][];
18311835
};
18321836
};
18331837
};

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { useForm } from "@mantine/form";
2-
import { Button, Flex, formatDate, MonthPicker, Stack, TextInput } from "@quassel/ui";
2+
import { Button, Flex, formatDate, getNext, MonthPicker, Stack, TextInput } from "@quassel/ui";
33
import { i18n } from "../../stores/i18n";
44
import { useStore } from "@nanostores/react";
55
import { useEffect } from "react";
66
import { params } from "@nanostores/i18n";
77

88
export type PeriodFormValues = {
99
title: string;
10-
range: [Date, Date];
10+
range: [Date | null, Date | null];
1111
};
1212

1313
type PeriodFormProps = {
1414
onSave: (form: PeriodFormValues) => void;
1515
period?: PeriodFormValues;
16+
prevEndDate?: Date;
1617
actionLabel: string;
1718
};
1819

@@ -21,12 +22,14 @@ const messages = i18n("periodForm", {
2122
defaultTitle: params("Period ({start} - {end})"),
2223
});
2324

24-
export function PeriodForm({ onSave, actionLabel, period }: PeriodFormProps) {
25+
export function PeriodForm({ onSave, actionLabel, period, prevEndDate }: PeriodFormProps) {
2526
const t = useStore(messages);
2627

2728
const f = useForm<PeriodFormValues>({
28-
initialValues: period,
29-
mode: "uncontrolled",
29+
initialValues: {
30+
range: [prevEndDate ? getNext("month", prevEndDate) : null, null],
31+
title: "",
32+
},
3033
onValuesChange(newValues, prevValues) {
3134
const [newStart, newEnd] = newValues.range ?? [];
3235
const [prevStart, prevEnd] = prevValues.range ?? [];
@@ -48,7 +51,17 @@ export function PeriodForm({ onSave, actionLabel, period }: PeriodFormProps) {
4851
<form onSubmit={f.onSubmit((values) => onSave(values))}>
4952
<Stack>
5053
<Flex justify="center">
51-
<MonthPicker {...f.getInputProps("range")} size="md" type="range" numberOfColumns={2} selectEndOfMonth />
54+
<MonthPicker
55+
{...f.getInputProps("range")}
56+
size="md"
57+
type="range"
58+
minDate={prevEndDate}
59+
defaultDate={prevEndDate}
60+
numberOfColumns={2}
61+
columnsToScroll={1}
62+
allowSingleDateInRange
63+
selectEndOfMonth
64+
/>
5265
</Flex>
5366
<TextInput {...f.getInputProps("title")} label={t.labelTitle} />
5467
<Button type="submit">{actionLabel}</Button>

apps/frontend/src/routes/_auth/questionnaire/_questionnaire/$id/period.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ function QuestionnairePeriod() {
3535
range: [localStartedAt, localEndedAt],
3636
} = form;
3737

38-
const startedAt = localStartedAt.toISOString();
39-
const endedAt = localEndedAt.toISOString();
38+
const startedAt = localStartedAt!.toISOString();
39+
const endedAt = localEndedAt!.toISOString();
4040

4141
await updateQuestionnaireMutation.mutateAsync({
4242
params: { path: { id } },

apps/frontend/src/routes/_auth/questionnaire/_questionnaire/new.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ function QuestionnaireNew() {
2222
},
2323
});
2424

25+
const prevEndDate = questionnaire?.participant.latestQuestionnaire?.endedAt;
26+
2527
const onSave = (form: PeriodFormValues) => {
2628
const {
2729
title,
2830
range: [localStartedAt, localEndedAt],
2931
} = form;
3032

31-
const startedAt = localStartedAt.toISOString();
32-
const endedAt = localEndedAt.toISOString();
33+
const startedAt = localStartedAt!.toISOString();
34+
const endedAt = localEndedAt!.toISOString();
3335

3436
createQuestionnaireMutation.mutate({
3537
body: { title, startedAt, endedAt, study: questionnaire!.study.id, participant: questionnaire!.participant.id },
@@ -39,7 +41,7 @@ function QuestionnaireNew() {
3941
return (
4042
<>
4143
<h3>{t.title}</h3>
42-
<PeriodForm onSave={onSave} actionLabel={t.formAction} />
44+
<PeriodForm onSave={onSave} prevEndDate={prevEndDate ? new Date(prevEndDate) : undefined} actionLabel={t.formAction} />
4345
</>
4446
);
4547
}

libs/ui/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,4 @@ export {
8181
IconMinus,
8282
} from "@tabler/icons-react";
8383

84-
export { formatDate, getTime, getDateFromTimeAndWeekday } from "./utils/date";
84+
export { formatDate, getTime, getDateFromTimeAndWeekday, getNext } from "./utils/date";

libs/ui/src/utils/date.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export function formatDate(date: Date, dayjsFormatTemplate: string) {
66

77
export const getTime = (date: Date) => formatDate(date, "HH:mm");
88

9+
export const getNext = (unit: dayjs.ManipulateType, date: Date) => {
10+
return dayjs(date).add(1, unit).startOf(unit).toDate();
11+
};
12+
913
export function getDateFromTimeAndWeekday(time: string, weekday: number) {
1014
return dayjs(time, "HH:mm:ss")
1115
.set("day", weekday)

0 commit comments

Comments
 (0)