Skip to content

Commit ddf0e85

Browse files
committed
refactor: extract calendar to separate component
1 parent 5ce1642 commit ddf0e85

File tree

3 files changed

+164
-120
lines changed

3 files changed

+164
-120
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import FullCalendar from "@fullcalendar/react";
2+
import interactionPlugin, { EventResizeStopArg } from "@fullcalendar/interaction";
3+
import timeGridPlugin from "@fullcalendar/timegrid";
4+
import { EventDropArg, EventInput } from "@fullcalendar/core";
5+
import { formatDate, getDateFromTimeAndWeekday, getTime, isSame, Modal, useDisclosure, useMantineTheme } from "@quassel/ui";
6+
import { QuestionnaireEntry } from "./QuestionnaireEntry";
7+
import { components } from "../../../api.gen";
8+
import { EntityForm, EntryFormValues } from "./EntryForm";
9+
import { useState } from "react";
10+
import { i18n } from "../../../stores/i18n";
11+
import { useStore } from "@nanostores/react";
12+
13+
const calendarBaseConfig: FullCalendar["props"] = {
14+
allDaySlot: false,
15+
headerToolbar: false,
16+
slotMinTime: { hour: 5 },
17+
slotMaxTime: { hour: 23 },
18+
slotDuration: { hour: 1 },
19+
firstDay: 1,
20+
dayHeaderContent: ({ date }) => formatDate(date, "dddd"),
21+
locale: "de",
22+
expandRows: true,
23+
editable: true,
24+
selectAllow: ({ start, end }) => isSame("day", start, end),
25+
selectable: true,
26+
selectLongPressDelay: 200,
27+
eventLongPressDelay: 400,
28+
};
29+
30+
export type ExtendedEvent = EventInput & { extendedProps: { entryLanguages: components["schemas"]["EntryLanguageResponseDto"][] } };
31+
32+
export type EntryCalendarProps = {
33+
entries: components["schemas"]["QuestionnaireEntryDto"][];
34+
onAddEntry: (entry: EntryFormValues, weekday: number) => Promise<unknown>;
35+
onUpdateEntry: (id: number, entry: Partial<EntryFormValues>, weekday: number) => Promise<unknown>;
36+
onDeleteEntry: (id: number) => Promise<unknown>;
37+
carers: components["schemas"]["CarerDto"][];
38+
languages: components["schemas"]["LanguageDto"][];
39+
onAddCarer: (value: string) => Promise<number>;
40+
onAddLanguage: (value: string) => Promise<number>;
41+
};
42+
43+
const messages = i18n("entryCalendar", {
44+
actionAdd: "Add",
45+
actionUpdate: "Update",
46+
});
47+
48+
export function EntryCalendar({
49+
entries,
50+
onAddEntry,
51+
onUpdateEntry,
52+
onDeleteEntry,
53+
carers,
54+
languages,
55+
onAddCarer,
56+
onAddLanguage,
57+
}: EntryCalendarProps) {
58+
const theme = useMantineTheme();
59+
60+
const t = useStore(messages);
61+
62+
const [opened, { open, close }] = useDisclosure();
63+
64+
const [selectedWeekday, setSelectedWeekday] = useState<number>();
65+
const [entryUpdatingId, setEntryUpdadingId] = useState<number>();
66+
const [entryDraft, setEntryDraft] = useState<Partial<EntryFormValues>>();
67+
68+
const events: ExtendedEvent[] =
69+
entries.map(({ startedAt, endedAt, weekday, carer, entryLanguages, id }) => ({
70+
id: id.toString(),
71+
start: getDateFromTimeAndWeekday(startedAt, weekday),
72+
end: getDateFromTimeAndWeekday(endedAt, weekday),
73+
title: carer.name,
74+
extendedProps: { entryLanguages },
75+
backgroundColor: theme.colors[theme.primaryColor][4],
76+
})) ?? [];
77+
78+
const handleMove = ({ event: { id, start, end } }: EventDropArg | EventResizeStopArg) => {
79+
onUpdateEntry(parseInt(id), { startedAt: getTime(start!), endedAt: getTime(end!) }, start!.getDay());
80+
};
81+
82+
const handleOnSave = async (entry: EntryFormValues) => {
83+
if (!selectedWeekday) return;
84+
85+
if (!entryUpdatingId) {
86+
await onAddEntry(entry, selectedWeekday);
87+
} else {
88+
await onUpdateEntry(entryUpdatingId, entry, selectedWeekday);
89+
}
90+
close();
91+
};
92+
93+
return (
94+
<>
95+
<Modal opened={opened} onClose={close} size="md">
96+
<EntityForm
97+
onAddCarer={onAddCarer}
98+
onAddLanguage={onAddLanguage}
99+
onSave={handleOnSave}
100+
onDelete={entryUpdatingId ? () => onDeleteEntry(entryUpdatingId).then(close) : undefined}
101+
entry={entryDraft}
102+
carers={carers}
103+
languages={languages}
104+
actionLabel={entryUpdatingId ? t.actionUpdate : t.actionAdd}
105+
/>
106+
</Modal>
107+
<FullCalendar
108+
{...calendarBaseConfig}
109+
plugins={[timeGridPlugin, interactionPlugin]}
110+
events={events}
111+
select={({ start, end }) => {
112+
setEntryDraft({ startedAt: getTime(start), endedAt: getTime(end) });
113+
setSelectedWeekday(start.getDay());
114+
open();
115+
}}
116+
eventClick={({ event }) => {
117+
const { carer, entryLanguages, id, weekday, ...rest } = entries?.find((entry) => entry.id.toString() === event.id) ?? {};
118+
119+
setEntryDraft({
120+
carer: carer?.id,
121+
entryLanguages: entryLanguages?.map(({ language, ...rest }) => ({ ...rest, language: language.id })),
122+
...rest,
123+
startedAt: getTime(event.start!),
124+
endedAt: getTime(event.end!),
125+
});
126+
setSelectedWeekday(weekday);
127+
setEntryUpdadingId(id);
128+
open();
129+
}}
130+
eventResize={handleMove}
131+
eventDrop={handleMove}
132+
eventContent={({ event }) => <QuestionnaireEntry event={event} />}
133+
/>
134+
</>
135+
);
136+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,12 @@ export function EntityForm({ onSave, onDelete, onAddCarer, onAddLanguage, action
102102
};
103103

104104
return (
105-
<form onSubmit={f.onSubmit(onSave)}>
105+
<form
106+
onSubmit={(event) => {
107+
f.onSubmit(onSave)(event);
108+
event.stopPropagation();
109+
}}
110+
>
106111
<Stack>
107112
<CarerSelect data={carers} {...f.getInputProps("carer")} onAddNew={onAddCarer} placeholder={t.labelCarer} />
108113

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

Lines changed: 22 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,11 @@
1-
import {
2-
Button,
3-
formatDate,
4-
getDateFromTimeAndWeekday,
5-
Group,
6-
Stack,
7-
useMantineTheme,
8-
useDisclosure,
9-
Modal,
10-
getTime,
11-
notifications,
12-
isSame,
13-
} from "@quassel/ui";
1+
import { Button, Group, Stack, notifications } from "@quassel/ui";
142
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
153
import { i18n } from "../../../../../stores/i18n";
164
import { useStore } from "@nanostores/react";
175
import { $api } from "../../../../../stores/api";
18-
import FullCalendar from "@fullcalendar/react";
19-
import timeGridPlugin from "@fullcalendar/timegrid";
20-
import { EventInput } from "@fullcalendar/core";
21-
import interactionPlugin from "@fullcalendar/interaction";
22-
import { components } from "../../../../../api.gen";
23-
import { QuestionnaireEntry } from "../../../../../components/questionnaire/calendar/QuestionnaireEntry";
24-
import { EntityForm, EntryFormValues } from "../../../../../components/questionnaire/calendar/EntryForm";
25-
import { useState } from "react";
6+
import { EntryFormValues } from "../../../../../components/questionnaire/calendar/EntryForm";
267
import { useQueryClient } from "@tanstack/react-query";
27-
28-
export type ExtendedEvent = EventInput & { extendedProps: { entryLanguages: components["schemas"]["EntryLanguageResponseDto"][] } };
29-
30-
const calendarBaseConfig: FullCalendar["props"] = {
31-
allDaySlot: false,
32-
headerToolbar: false,
33-
slotMinTime: { hour: 5 },
34-
slotMaxTime: { hour: 23 },
35-
slotDuration: { hour: 1 },
36-
firstDay: 1,
37-
dayHeaderContent: ({ date }) => formatDate(date, "dddd"),
38-
locale: "de",
39-
expandRows: true,
40-
};
8+
import { EntryCalendar } from "../../../../../components/questionnaire/calendar/EntryCalendar";
419

4210
const messages = i18n("questionnaireEntries", {
4311
formAction: "Continue",
@@ -55,17 +23,10 @@ function QuestionnaireEntries() {
5523

5624
const c = useQueryClient();
5725

58-
const theme = useMantineTheme();
59-
const [opened, { open, close }] = useDisclosure();
60-
61-
const [selectedWeekday, setSelectedWeekday] = useState<number>();
62-
const [entryUpdatingId, setEntryUpdadingId] = useState<number>();
63-
const [entryDraft, setEntryDraft] = useState<Partial<EntryFormValues>>();
64-
6526
const createMutation = $api.useMutation("post", "/entries");
6627
const updateMutation = $api.useMutation("patch", "/entries/{id}");
6728
const deleteMutation = $api.useMutation("delete", "/entries/{id}");
68-
const { data: questionnaire, refetch } = $api.useSuspenseQuery("get", "/questionnaires/{id}", { params: { path: { id: p.id } } });
29+
const { data: questionnaire } = $api.useSuspenseQuery("get", "/questionnaires/{id}", { params: { path: p } });
6930

7031
const participantId = questionnaire.participant?.id;
7132

@@ -85,57 +46,34 @@ function QuestionnaireEntries() {
8546
},
8647
});
8748

88-
const events: ExtendedEvent[] =
89-
questionnaire.entries?.map(({ startedAt, endedAt, weekday, carer, entryLanguages, id }) => ({
90-
id: id.toString(),
91-
start: getDateFromTimeAndWeekday(startedAt, weekday),
92-
end: getDateFromTimeAndWeekday(endedAt, weekday),
93-
title: carer.name,
94-
extendedProps: { entryLanguages },
95-
backgroundColor: theme.colors[theme.primaryColor][4],
96-
})) ?? [];
97-
98-
const reset = () => {
99-
refetch();
100-
close();
101-
setSelectedWeekday(undefined);
102-
setEntryUpdadingId(undefined);
49+
const reloadEntries = () => {
50+
c.invalidateQueries($api.queryOptions("get", "/questionnaires/{id}", { params: { path: p } }));
10351
};
10452

105-
const handleCreate = ({ carer, ...rest }: EntryFormValues) => {
106-
if (selectedWeekday === undefined) return;
107-
53+
const handleCreate = ({ carer, ...rest }: EntryFormValues, weekday: number) => {
10854
const entryRequest = {
10955
...rest,
11056
carer: carer!,
111-
weekday: selectedWeekday,
57+
weekday,
11258
questionnaire: questionnaire.id,
11359
};
11460

115-
createMutation.mutate({ body: entryRequest }, { onSuccess: reset });
61+
return createMutation.mutateAsync({ body: entryRequest }, { onSuccess: reloadEntries });
11662
};
11763

118-
const handleUpdate = (id: number, { carer, ...rest }: Partial<EntryFormValues>, weekday?: number) => {
64+
const handleUpdate = (id: number, { carer, ...rest }: Partial<EntryFormValues>, weekday: number) => {
11965
const entryRequest = {
12066
...rest,
12167
carer: carer!,
12268
weekday,
12369
questionnaire: questionnaire.id,
12470
};
12571

126-
updateMutation.mutate({ body: entryRequest, params: { path: { id: id.toString() } } }, { onSuccess: reset });
72+
return updateMutation.mutateAsync({ body: entryRequest, params: { path: { id: id.toString() } } }, { onSuccess: reloadEntries });
12773
};
12874

12975
const handleDelete = (id: number) => {
130-
deleteMutation.mutate({ params: { path: { id: id.toString() } } }, { onSuccess: reset });
131-
};
132-
133-
const handleOnSave = (entry: EntryFormValues | Partial<EntryFormValues>) => {
134-
if (!entryUpdatingId) {
135-
handleCreate(entry as EntryFormValues);
136-
} else {
137-
handleUpdate(entryUpdatingId, entry);
138-
}
76+
return deleteMutation.mutateAsync({ params: { path: { id: id.toString() } } }, { onSuccess: reloadEntries });
13977
};
14078

14179
const handleSubmit = () => {
@@ -144,54 +82,19 @@ function QuestionnaireEntries() {
14482

14583
return (
14684
<>
147-
<Modal opened={opened} onClose={close} size="md">
148-
<EntityForm
149-
onAddCarer={(name) => createCarerMutation.mutateAsync({ body: { name, participant: participantId } }).then(({ id }) => id)}
150-
onAddLanguage={(name) => createLanguageMutation.mutateAsync({ body: { name, participant: participantId } }).then(({ id }) => id)}
151-
onSave={handleOnSave}
152-
onDelete={entryUpdatingId ? () => handleDelete(entryUpdatingId) : undefined}
153-
entry={entryDraft}
154-
carers={carers ?? []}
155-
languages={languages ?? []}
156-
actionLabel={t.addEntityLabel}
157-
/>
158-
</Modal>
15985
<form onSubmit={handleSubmit}>
16086
<Stack>
161-
<FullCalendar
162-
{...calendarBaseConfig}
163-
plugins={[timeGridPlugin, interactionPlugin]}
164-
editable
165-
events={events}
166-
selectAllow={({ start, end }) => isSame("day", start, end)}
167-
selectable
168-
select={({ start, end }) => {
169-
setEntryDraft({ startedAt: getTime(start), endedAt: getTime(end) });
170-
setSelectedWeekday(start.getDay());
171-
open();
172-
}}
173-
eventClick={(args) => {
174-
const { carer, entryLanguages, id, weeklyRecurring } =
175-
questionnaire.entries?.find((entry) => entry.id.toString() === args.event.id) ?? {};
176-
177-
setEntryDraft({
178-
carer: carer?.id,
179-
startedAt: getTime(args.event.start!),
180-
endedAt: getTime(args.event.end!),
181-
entryLanguages: entryLanguages?.map(({ language, ...rest }) => ({ ...rest, language: language.id })),
182-
weeklyRecurring,
183-
});
184-
setEntryUpdadingId(id);
185-
open();
186-
}}
187-
eventResize={({ event: { id, start, end } }) => {
188-
handleUpdate(parseInt(id), { startedAt: getTime(start!), endedAt: getTime(end!) });
189-
}}
190-
eventDrop={({ event: { id, start, end } }) => {
191-
handleUpdate(parseInt(id), { startedAt: getTime(start!), endedAt: getTime(end!) }, start!.getDay());
192-
}}
193-
eventContent={({ event }) => <QuestionnaireEntry event={event} />}
87+
<EntryCalendar
88+
entries={questionnaire.entries ?? []}
89+
onAddEntry={handleCreate}
90+
onUpdateEntry={handleUpdate}
91+
onDeleteEntry={handleDelete}
92+
carers={carers ?? []}
93+
languages={languages ?? []}
94+
onAddCarer={(name) => createCarerMutation.mutateAsync({ body: { name, participant: participantId } }).then(({ id }) => id)}
95+
onAddLanguage={(name) => createLanguageMutation.mutateAsync({ body: { name, participant: participantId } }).then(({ id }) => id)}
19496
/>
97+
19598
<Group>
19699
<Link to="/questionnaire/$id/period" params={p}>
197100
<Button variant="light">{t.backAction}</Button>

0 commit comments

Comments
 (0)