Skip to content

Commit e9252f4

Browse files
authored
Merge pull request #189 from openscript-ch/47-finish-a-questionnaire-when-there-is-no-other-period
47 finish a questionnaire when there is no other period
2 parents b016240 + 389c4ee commit e9252f4

File tree

22 files changed

+656
-196
lines changed

22 files changed

+656
-196
lines changed

apps/backend/db/migrations/.snapshot-postgres.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@
445445
"unsigned": false,
446446
"autoincrement": false,
447447
"primary": false,
448-
"nullable": false,
448+
"nullable": true,
449449
"mappedType": "integer"
450450
},
451451
"carer_id": {
@@ -497,6 +497,7 @@
497497
"id"
498498
],
499499
"referencedTableName": "public.questionnaire",
500+
"deleteRule": "cascade",
500501
"updateRule": "cascade"
501502
},
502503
"entry_carer_id_foreign": {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Migration } from "@mikro-orm/migrations";
2+
3+
export class Migration20241211083949 extends Migration {
4+
override async up(): Promise<void> {
5+
this.addSql(`alter table "entry" drop constraint "entry_questionnaire_id_foreign";`);
6+
7+
this.addSql(`alter table "entry" alter column "questionnaire_id" type int using ("questionnaire_id"::int);`);
8+
this.addSql(`alter table "entry" alter column "questionnaire_id" drop not null;`);
9+
this.addSql(
10+
`alter table "entry" add constraint "entry_questionnaire_id_foreign" foreign key ("questionnaire_id") references "questionnaire" ("id") on update cascade on delete cascade;`
11+
);
12+
}
13+
14+
override async down(): Promise<void> {
15+
this.addSql(`alter table "entry" drop constraint "entry_questionnaire_id_foreign";`);
16+
17+
this.addSql(`alter table "entry" alter column "questionnaire_id" type int using ("questionnaire_id"::int);`);
18+
this.addSql(`alter table "entry" alter column "questionnaire_id" set not null;`);
19+
this.addSql(
20+
`alter table "entry" add constraint "entry_questionnaire_id_foreign" foreign key ("questionnaire_id") references "questionnaire" ("id") on update cascade;`
21+
);
22+
}
23+
}

apps/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@nestjs/terminus": "^10.2.3",
4343
"class-transformer": "^0.5.1",
4444
"class-validator": "^0.14.1",
45+
"date-fns": "^4.1.0",
4546
"fastify": "^4.28.1",
4647
"hash-wasm": "^4.12.0",
4748
"reflect-metadata": "^0.2.2",

apps/backend/src/research/entries/entry.entity.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Check, Collection, Entity, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
1+
import { Cascade, Check, Collection, Entity, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
22
import { BaseEntity } from "../../common/entities/base.entity";
33
import { Carer } from "../../defaults/carers/carer.entity";
44
import { EntryLanguage } from "../entry-languages/entry-language.entity";
@@ -23,7 +23,7 @@ export class Entry extends BaseEntity {
2323
@Check<Entry>({ expression: (columns) => `${columns.weeklyRecurring} >= 1` })
2424
weeklyRecurring!: number & Opt;
2525

26-
@ManyToOne()
26+
@ManyToOne({ cascade: [Cascade.ALL] })
2727
questionnaire!: Questionnaire;
2828

2929
@ManyToOne()

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/backend/src/research/questionnaires/questionnaires.service.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { InjectRepository } from "@mikro-orm/nestjs";
33
import { Injectable, UnprocessableEntityException } from "@nestjs/common";
44
import { QuestionnaireCreationDto, QuestionnaireMutationDto } from "./questionnaire.dto";
55
import { Questionnaire } from "./questionnaire.entity";
6+
import { Entry } from "../entries/entry.entity";
7+
import { EntryLanguage } from "../entry-languages/entry-language.entity";
8+
import { addDays, isSameDay } from "date-fns";
69

710
@Injectable()
811
export class QuestionnairesService {
@@ -16,6 +19,25 @@ export class QuestionnairesService {
1619
const questionnaire = new Questionnaire();
1720
questionnaire.assign(questionnaireCreationDto, { em: this.em });
1821

22+
const prevQuestionnaire = await this.findLatestByParticipant(questionnaire.participant!.id);
23+
this.validateStartDate(questionnaire, prevQuestionnaire);
24+
25+
await prevQuestionnaire?.populate(["entries.entryLanguages"]);
26+
27+
const clonedEntries = prevQuestionnaire?.entries.map((entry) => {
28+
const { id: _id, entryLanguages: _entryLanguages, questionnaire: _questionnaire, ...rest } = entry.toPOJO();
29+
const newEntry = this.em.create(Entry, { ...rest, questionnaire });
30+
31+
entry.entryLanguages.map((entryLanguage) => {
32+
const { id: _id, entry: _entry, ...rest } = entryLanguage.toPOJO();
33+
return this.em.create(EntryLanguage, { ...rest, entry: newEntry });
34+
});
35+
36+
return newEntry;
37+
});
38+
39+
questionnaire.assign({ entries: clonedEntries ?? [] });
40+
1941
try {
2042
await this.em.persist(questionnaire).flush();
2143
} catch (e) {
@@ -42,12 +64,21 @@ export class QuestionnairesService {
4264
return (await this.questionnaireRepository.findOneOrFail(filter)).toObject();
4365
}
4466

67+
async findLatestByParticipant(participantId: number) {
68+
return this.questionnaireRepository.findOne({ participant: participantId }, { orderBy: { endedAt: "desc" } });
69+
}
70+
4571
async update(id: number, questionnaireMutationDto: QuestionnaireMutationDto) {
4672
const questionnaire = await this.questionnaireRepository.findOneOrFail(id, {
4773
populate: ["entries", "entries.carer", "entries.entryLanguages.language"],
4874
});
4975
questionnaire.assign(questionnaireMutationDto);
5076

77+
const prevQuestionnaire = await this.findLatestByParticipant(questionnaire.participant!.id);
78+
if (prevQuestionnaire?.id !== id) {
79+
this.validateStartDate(questionnaire, prevQuestionnaire);
80+
}
81+
5182
await this.em.persist(questionnaire).flush();
5283

5384
return questionnaire.toObject();
@@ -56,4 +87,10 @@ export class QuestionnairesService {
5687
remove(id: number) {
5788
return this.em.remove(this.questionnaireRepository.getReference(id)).flush();
5889
}
90+
91+
private validateStartDate(questionnaire: Questionnaire, prevQuestionnaire: Questionnaire | null) {
92+
if (prevQuestionnaire && !isSameDay(addDays(prevQuestionnaire.endedAt!, 1), questionnaire.startedAt!)) {
93+
throw new UnprocessableEntityException("Start of the new questionnaire must match with the end of the previous");
94+
}
95+
}
5996
}

apps/frontend/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,22 @@
2020
"@fullcalendar/interaction": "^6.1.15",
2121
"@fullcalendar/react": "^6.1.15",
2222
"@fullcalendar/timegrid": "^6.1.15",
23-
"@mantine/form": "^7.15.0",
23+
"@mantine/form": "^7.15.1",
2424
"@nanostores/i18n": "^0.12.2",
2525
"@nanostores/persistent": "^0.10.2",
2626
"@nanostores/react": "^0.8.2",
2727
"@quassel/ui": "workspace:*",
28-
"@tanstack/react-query": "^5.62.3",
29-
"@tanstack/react-router": "^1.87.3",
28+
"@tanstack/react-query": "^5.62.7",
29+
"@tanstack/react-router": "^1.88.0",
3030
"nanostores": "^0.11.3",
3131
"openapi-fetch": "0.13.3",
3232
"openapi-react-query": "0.2.8",
3333
"react": "^18.3.1",
3434
"react-dom": "^18.3.1"
3535
},
3636
"devDependencies": {
37-
"@tanstack/router-devtools": "^1.87.3",
38-
"@tanstack/router-plugin": "^1.87.4",
37+
"@tanstack/router-devtools": "^1.88.0",
38+
"@tanstack/router-plugin": "^1.87.13",
3939
"@testing-library/jest-dom": "^6.6.3",
4040
"@testing-library/react": "^16.1.0",
4141
"@types/react": "^18.3.1",

0 commit comments

Comments
 (0)