Skip to content

Commit fe20ab9

Browse files
committed
feat: allow creating entries for multiple days
1 parent 2af058e commit fe20ab9

File tree

9 files changed

+118
-111
lines changed

9 files changed

+118
-111
lines changed

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post } from "@nestjs/commo
22
import { ApiTags, ApiOperation, ApiUnprocessableEntityResponse } from "@nestjs/swagger";
33
import { ErrorResponseDto } from "../../common/dto/error.dto";
44
import { EntriesService } from "./entries.service";
5-
import { EntryCreationDto, EntryResponseDto, EntryMutationDto } from "./entry.dto";
5+
import { EntryCreationDto, EntryResponseDto, EntryUpdateDto } from "./entry.dto";
66
import { Serialize } from "../../common/decorators/serialize";
77

88
@ApiTags("Entries")
@@ -13,8 +13,7 @@ export class EntriesController {
1313
@Post()
1414
@ApiOperation({ summary: "Create a entry" })
1515
@ApiUnprocessableEntityResponse({ description: "Unique name constraint violation", type: ErrorResponseDto })
16-
@Serialize(EntryResponseDto)
17-
create(@Body() entry: EntryCreationDto): Promise<EntryResponseDto> {
16+
create(@Body() entry: EntryCreationDto): Promise<number[]> {
1817
return this.entriesService.create(entry);
1918
}
2019

@@ -35,7 +34,7 @@ export class EntriesController {
3534
@Patch(":id")
3635
@ApiOperation({ summary: "Update a entry by ID" })
3736
@Serialize(EntryResponseDto)
38-
update(@Param("id") id: string, @Body() entry: EntryMutationDto): Promise<EntryResponseDto> {
37+
update(@Param("id") id: string, @Body() entry: EntryUpdateDto): Promise<EntryResponseDto> {
3938
return this.entriesService.update(+id, entry);
4039
}
4140

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

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { EntityRepository, UniqueConstraintViolationException, FilterQuery } from "@mikro-orm/core";
1+
import { EntityRepository, FilterQuery } from "@mikro-orm/core";
22
import { InjectRepository } from "@mikro-orm/nestjs";
3-
import { Injectable, UnprocessableEntityException } from "@nestjs/common";
4-
import { EntryCreationDto, EntryMutationDto } from "./entry.dto";
3+
import { Injectable } from "@nestjs/common";
4+
import { EntryCreationDto, EntryUpdateDto } from "./entry.dto";
55
import { Entry } from "./entry.entity";
66
import { EntityManager, raw } from "@mikro-orm/postgresql";
77

@@ -13,20 +13,15 @@ export class EntriesService {
1313
private readonly em: EntityManager
1414
) {}
1515

16-
async create(entryCreationDto: EntryCreationDto) {
17-
const entry = new Entry();
18-
entry.assign(entryCreationDto, { em: this.em });
16+
create({ weekday, ...rest }: EntryCreationDto) {
17+
return this.entryRepository.insertMany(
18+
weekday.map((w) => {
19+
const entry = new Entry();
20+
entry.assign({ weekday: w, ...rest }, { em: this.em });
1921

20-
try {
21-
await this.em.persist(entry).flush();
22-
} catch (e) {
23-
if (e instanceof UniqueConstraintViolationException) {
24-
throw new UnprocessableEntityException("Entry with this name already exists");
25-
}
26-
throw e;
27-
}
28-
29-
return (await entry.populate(["entryLanguages"])).toObject();
22+
return entry;
23+
})
24+
);
3025
}
3126

3227
async findAll() {
@@ -66,7 +61,7 @@ export class EntriesService {
6661
});
6762
}
6863

69-
async update(id: number, entryMutationDto: EntryMutationDto) {
64+
async update(id: number, entryMutationDto: EntryUpdateDto) {
7065
const entry = await this.entryRepository.findOneOrFail(id, { populate: ["entryLanguages"] });
7166

7267
entry.assign(entryMutationDto);

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@ class EntryBaseDto {
1515
@Expose()
1616
endedAt: string;
1717

18-
@ApiProperty({ example: 1, description: "The weekday of the entry (Sunday is 0 like in JS)" })
19-
@Min(0)
20-
@Max(6)
21-
@Expose()
22-
weekday: number;
23-
2418
@ApiProperty({ example: 1, description: "The weekly recurring of the entry" })
2519
@Min(1)
2620
@IsOptional()
@@ -32,6 +26,12 @@ export class EntryResponseDto extends EntryBaseDto {
3226
@Expose()
3327
id: number;
3428

29+
@ApiProperty({ example: 1, description: "The weekday of the entry (Sunday is 0 like in JS)" })
30+
@Min(0)
31+
@Max(6)
32+
@Expose()
33+
weekday: number;
34+
3535
@Type(() => CarerResponseDto)
3636
@Expose()
3737
carer: CarerResponseDto;
@@ -41,14 +41,17 @@ export class EntryResponseDto extends EntryBaseDto {
4141
entryLanguages: Array<EntryLanguageResponseDto>;
4242
}
4343

44-
export class EntryCreationDto extends EntryBaseDto {
44+
class EntryMutationDto extends EntryBaseDto {
4545
carer: number;
4646
questionnaire: number;
4747

4848
@Type(() => EntryLanguageCreationDto)
4949
entryLanguages: Array<EntryLanguageCreationDto>;
5050
}
51-
export class EntryMutationDto extends PartialType(EntryCreationDto) {}
51+
export class EntryCreationDto extends EntryMutationDto {
52+
weekday: number[];
53+
}
54+
export class EntryUpdateDto extends PartialType(EntryMutationDto) {}
5255

5356
export class EntryTemplateDto {
5457
@Type(() => CarerResponseDto)

apps/frontend/src/api.gen.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -740,16 +740,12 @@ export interface components {
740740
* @example 2024-11-01T08:00:00.00Z
741741
*/
742742
endedAt: string;
743-
/**
744-
* @description The weekday of the entry (Sunday is 0 like in JS)
745-
* @example 1
746-
*/
747-
weekday: number;
748743
/**
749744
* @description The weekly recurring of the entry
750745
* @example 1
751746
*/
752747
weeklyRecurring?: number;
748+
weekday: number[];
753749
carer: number;
754750
questionnaire: number;
755751
entryLanguages: components["schemas"]["EntryLanguageCreationDto"][];
@@ -765,11 +761,6 @@ export interface components {
765761
* @example 2024-11-01T08:00:00.00Z
766762
*/
767763
endedAt: string;
768-
/**
769-
* @description The weekday of the entry (Sunday is 0 like in JS)
770-
* @example 1
771-
*/
772-
weekday: number;
773764
/**
774765
* @description The weekly recurring of the entry
775766
* @example 1
@@ -780,10 +771,15 @@ export interface components {
780771
* @example 1
781772
*/
782773
id: number;
774+
/**
775+
* @description The weekday of the entry (Sunday is 0 like in JS)
776+
* @example 1
777+
*/
778+
weekday: number;
783779
carer: components["schemas"]["CarerResponseDto"];
784780
entryLanguages: components["schemas"]["EntryLanguageResponseDto"][];
785781
};
786-
EntryMutationDto: {
782+
EntryUpdateDto: {
787783
/**
788784
* @description The starting date of the entry
789785
* @example 2024-11-01T07:00:00.000Z
@@ -794,11 +790,6 @@ export interface components {
794790
* @example 2024-11-01T08:00:00.00Z
795791
*/
796792
endedAt?: string;
797-
/**
798-
* @description The weekday of the entry (Sunday is 0 like in JS)
799-
* @example 1
800-
*/
801-
weekday?: number;
802793
/**
803794
* @description The weekly recurring of the entry
804795
* @example 1
@@ -1828,7 +1819,7 @@ export interface operations {
18281819
[name: string]: unknown;
18291820
};
18301821
content: {
1831-
"application/json": components["schemas"]["EntryResponseDto"];
1822+
"application/json": number[];
18321823
};
18331824
};
18341825
/** @description Unique name constraint violation */
@@ -1893,7 +1884,7 @@ export interface operations {
18931884
};
18941885
requestBody: {
18951886
content: {
1896-
"application/json": components["schemas"]["EntryMutationDto"];
1887+
"application/json": components["schemas"]["EntryUpdateDto"];
18971888
};
18981889
};
18991890
responses: {
Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,53 @@
1-
import { SegmentedControl } from "@quassel/ui";
1+
import { Chip, Group, Select } from "@quassel/ui";
22
import { i18n } from "../stores/i18n";
33
import { useStore } from "@nanostores/react";
44

5-
type WeekdayPickerProps = {
6-
value?: number;
7-
onChange?: (weekday: number) => void;
8-
};
5+
type WeekdayPickerProps =
6+
| { value?: number; onChange?: (weekday?: number) => void; multiple?: false }
7+
| { value?: number[]; onChange?: (weekday: number[]) => void; multiple: true };
98

109
const messages = i18n("WeekdayPicker", {
11-
mondayLabel: "Mo",
12-
tusedayLabel: "Tu",
13-
wednesdayLabel: "We",
14-
thrusdayLabel: "Th",
15-
fridayLabel: "Fr",
16-
saturdayLabel: "Sa",
17-
sundayLabel: "Su",
10+
mondayShortLabel: "Mo",
11+
mondayLabel: "Monday",
12+
tusedayShortLabel: "Tu",
13+
tusedayLabel: "Tuesday",
14+
wednesdayShortLabel: "We",
15+
wednesdayLabel: "Wednesday",
16+
thursdayShortLabel: "Th",
17+
thursdayLabel: "Thursday",
18+
fridayShortLabel: "Fr",
19+
fridayLabel: "Friday",
20+
saturdayShortLabel: "Sa",
21+
saturdayLabel: "Saturday",
22+
sundayShortLabel: "Su",
23+
sundayLabel: "Sunday",
1824
});
1925

20-
export function WeekdayPicker({ onChange, value }: WeekdayPickerProps) {
26+
export function WeekdayPicker({ onChange, value, multiple }: WeekdayPickerProps) {
2127
const t = useStore(messages);
2228

23-
return (
24-
<SegmentedControl
25-
value={value?.toString()}
26-
onChange={(value) => onChange?.(parseInt(value))}
27-
data={[
28-
{ value: "1", label: t.mondayLabel },
29-
{ value: "2", label: t.tusedayLabel },
30-
{ value: "3", label: t.wednesdayLabel },
31-
{ value: "4", label: t.thrusdayLabel },
32-
{ value: "5", label: t.fridayLabel },
33-
{ value: "6", label: t.saturdayLabel },
34-
{ value: "0", label: t.sundayLabel },
35-
]}
36-
/>
37-
);
29+
const weekdayOptions = [
30+
{ value: "1", label: t.mondayLabel, short: t.mondayShortLabel },
31+
{ value: "2", label: t.tusedayLabel, short: t.tusedayShortLabel },
32+
{ value: "3", label: t.wednesdayLabel, short: t.wednesdayShortLabel },
33+
{ value: "4", label: t.thursdayLabel, short: t.thursdayShortLabel },
34+
{ value: "5", label: t.fridayLabel, short: t.fridayShortLabel },
35+
{ value: "6", label: t.saturdayLabel, short: t.saturdayShortLabel },
36+
{ value: "0", label: t.sundayLabel, short: t.sundayShortLabel },
37+
];
38+
39+
if (multiple)
40+
return (
41+
<Chip.Group multiple value={value?.map((v) => v.toString())} onChange={(values) => onChange?.(values.map(Number))}>
42+
<Group gap={"xs"}>
43+
{weekdayOptions.map(({ value, short }) => (
44+
<Chip key={value} value={value.toString()}>
45+
{short}
46+
</Chip>
47+
))}
48+
</Group>
49+
</Chip.Group>
50+
);
51+
52+
return <Select data={weekdayOptions} value={value?.toString()} onChange={(v) => onChange?.(v ? parseInt(v) : undefined)} />;
3853
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function QuestionnaireEntries({ questionnaire, gaps }: QuestionnaireEntri
6262
const handleCreate = (entry: EntryFormValues) => {
6363
const entryRequest = { ...entry, questionnaire: questionnaire.id };
6464

65-
return createMutation.mutateAsync({ body: entryRequest }, { onSuccess: reloadEntries });
65+
return createMutation.mutateAsync({ body: entryRequest as components["schemas"]["EntryCreationDto"] }, { onSuccess: reloadEntries });
6666
};
6767

6868
const handleUpdate = (id: number, entry: Partial<EntryFormValues>) => {

apps/frontend/src/components/questionnaire/calendar/EntryCalendar.tsx

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ export function EntryCalendar({
7777

7878
const [opened, { open, close }] = useDisclosure();
7979

80-
const [entryUpdatingId, setEntryUpdadingId] = useState<number>();
81-
const [entryDraft, setEntryDraft] = useState<Partial<EntryFormValues>>();
80+
const [entryDraft, setEntryDraft] = useState<{ id?: number; value: Partial<EntryFormValues> }>();
8281

8382
const [events, setEvents] = useState<ExtendedEvent[]>([]);
8483

@@ -134,23 +133,24 @@ export function EntryCalendar({
134133
const { carer, entryLanguages, id, weekday, ...rest } = entries?.find((entry) => entry.id.toString() === event.id) ?? {};
135134

136135
setEntryDraft({
137-
carer: carer?.id,
138-
entryLanguages: entryLanguages?.map(({ language, ...rest }) => ({ ...rest, language: language.id })),
139-
...rest,
140-
startedAt: getTime(event.start!),
141-
endedAt: getTime(event.end!),
142-
weekday,
136+
id,
137+
value: {
138+
carer: carer?.id,
139+
entryLanguages: entryLanguages?.map(({ language, ...rest }) => ({ ...rest, language: language.id })),
140+
...rest,
141+
startedAt: getTime(event.start!),
142+
endedAt: getTime(event.end!),
143+
weekday,
144+
},
143145
});
144-
setEntryUpdadingId(id);
145146
open();
146147
};
147148

148149
const setupEntryCreate = ({ start, end }: DateSelectArg | EventImpl) => {
149150
if (!start || !end) return;
150151
if (end.getHours() === 0 && end.getMinutes() === 0) end.setTime(end.getTime() - 1000);
151152

152-
setEntryDraft({ startedAt: getTime(start), endedAt: getTime(end), weekday: start!.getDay() });
153-
setEntryUpdadingId(undefined);
153+
setEntryDraft({ value: { startedAt: getTime(start), endedAt: getTime(end), weekday: [start!.getDay()] } });
154154
open();
155155
};
156156

@@ -170,28 +170,30 @@ export function EntryCalendar({
170170
};
171171

172172
const handleOnSave = async (entry: EntryFormValues) => {
173-
if (!entryUpdatingId) {
174-
await onAddEntry(entry);
173+
if (entryDraft?.id) {
174+
await onUpdateEntry(entryDraft.id, entry);
175175
} else {
176-
await onUpdateEntry(entryUpdatingId, entry);
176+
await onAddEntry(entry);
177177
}
178178
close();
179179
};
180180

181181
return (
182182
<>
183-
<Modal opened={opened} onClose={close} size="md">
184-
<EntityForm
185-
onAddCarer={onAddCarer}
186-
onAddLanguage={onAddLanguage}
187-
onSave={handleOnSave}
188-
onDelete={entryUpdatingId ? () => onDeleteEntry(entryUpdatingId).then(close) : undefined}
189-
entry={entryDraft}
190-
carers={carers}
191-
languages={languages}
192-
templates={templates}
193-
actionLabel={entryUpdatingId ? t.actionUpdate : t.actionAdd}
194-
/>
183+
<Modal opened={opened} onClose={close} size="lg">
184+
{entryDraft && (
185+
<EntityForm
186+
onAddCarer={onAddCarer}
187+
onAddLanguage={onAddLanguage}
188+
onSave={handleOnSave}
189+
onDelete={entryDraft?.id ? () => onDeleteEntry(entryDraft.id!).then(close) : undefined}
190+
entry={entryDraft.value}
191+
carers={carers}
192+
languages={languages}
193+
templates={templates}
194+
actionLabel={entryDraft?.id ? t.actionUpdate : t.actionAdd}
195+
/>
196+
)}
195197
</Modal>
196198
<FullCalendar
197199
{...calendarBaseConfig}
@@ -202,8 +204,7 @@ export function EntryCalendar({
202204
<Button
203205
variant="subtle"
204206
onClick={() => {
205-
setEntryDraft({ weekday: date.getDay() });
206-
setEntryUpdadingId(undefined);
207+
setEntryDraft({ value: { weekday: [date.getDay()] } });
207208
open();
208209
}}
209210
>

0 commit comments

Comments
 (0)