Skip to content

Commit e827832

Browse files
committed
feat: highlight gaps in calendar
1 parent 1a30ce5 commit e827832

File tree

4 files changed

+44
-26
lines changed

4 files changed

+44
-26
lines changed

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { EntityForm, EntryFormValues } from "./EntryForm";
99
import { useEffect, useState } from "react";
1010
import { i18n } from "../../../stores/i18n";
1111
import { useStore } from "@nanostores/react";
12+
import { GapsPerDay } from "../../../utils/entry";
1213

1314
const calendarBaseConfig: FullCalendar["props"] = {
1415
allDaySlot: false,
@@ -27,10 +28,13 @@ const calendarBaseConfig: FullCalendar["props"] = {
2728
eventLongPressDelay: 400,
2829
};
2930

30-
export type ExtendedEvent = EventInput & { extendedProps: { entryLanguages: components["schemas"]["EntryLanguageResponseDto"][] } };
31+
export type ExtendedEvent = EventInput & {
32+
extendedProps?: { entryLanguages: components["schemas"]["EntryLanguageResponseDto"][]; weeklyRecurring?: number };
33+
};
3134

3235
export type EntryCalendarProps = {
3336
entries: components["schemas"]["QuestionnaireEntryDto"][];
37+
gaps?: GapsPerDay;
3438
onAddEntry: (entry: EntryFormValues, weekday: number) => Promise<unknown>;
3539
onUpdateEntry: (id: number, entry: Partial<EntryFormValues>, weekday: number) => Promise<unknown>;
3640
onDeleteEntry: (id: number) => Promise<unknown>;
@@ -47,6 +51,7 @@ const messages = i18n("entryCalendar", {
4751

4852
export function EntryCalendar({
4953
entries,
54+
gaps,
5055
onAddEntry,
5156
onUpdateEntry,
5257
onDeleteEntry,
@@ -69,18 +74,26 @@ export function EntryCalendar({
6974

7075
useEffect(() => {
7176
if (entries) {
72-
setEvents(
73-
entries.map(({ startedAt, endedAt, weekday, carer, entryLanguages, id }) => ({
77+
setEvents([
78+
...entries.map(({ startedAt, endedAt, weekday, carer, entryLanguages, id }) => ({
7479
id: id.toString(),
7580
start: getDateFromTimeAndWeekday(startedAt, weekday),
7681
end: getDateFromTimeAndWeekday(endedAt, weekday),
7782
title: carer.name,
7883
extendedProps: { entryLanguages },
7984
backgroundColor: theme.colors[theme.primaryColor][4],
80-
})) ?? []
81-
);
85+
})),
86+
...(gaps ?? []).flatMap((dailyGaps, index) =>
87+
dailyGaps.map((gap) => ({
88+
start: getDateFromTimeAndWeekday(gap[0], index),
89+
end: getDateFromTimeAndWeekday(gap[1], index),
90+
backgroundColor: theme.colors.uzhBerry[4],
91+
display: "background",
92+
}))
93+
),
94+
]);
8295
}
83-
}, [entries]);
96+
}, [entries, gaps]);
8497

8598
const handleEventChange = ({ event }: EventChangeArg) => {
8699
const { id, start, end } = event;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const messages = i18n("questionnaireEntries", {
1414
});
1515

1616
export function QuestionnaireEntry({ event }: QuestionnaireEntryProps) {
17-
const { entryLanguages, weeklyRecurring } = event.extendedProps as ExtendedEvent["extendedProps"];
17+
const { entryLanguages, weeklyRecurring } = (event.extendedProps as ExtendedEvent["extendedProps"]) ?? {};
1818

1919
const t = useStore(messages);
2020

@@ -23,7 +23,7 @@ export function QuestionnaireEntry({ event }: QuestionnaireEntryProps) {
2323
<Text size="sm" fw="bold" truncate>
2424
{event.title}
2525
</Text>
26-
{entryLanguages.map(({ language }) => language.ietfBcp47).join(", ")}
26+
{entryLanguages?.map(({ language }) => language.ietfBcp47).join(", ")}
2727
{weeklyRecurring && weeklyRecurring > 1 && (
2828
<Text mt="sm" size="sm">
2929
<IconRepeat size={13} /> {t.labelRecurringWeekly({ weeks: weeklyRecurring })}

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

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { $api } from "../../../../../stores/api";
66
import { EntryFormValues } from "../../../../../components/questionnaire/calendar/EntryForm";
77
import { useQueryClient } from "@tanstack/react-query";
88
import { EntryCalendar } from "../../../../../components/questionnaire/calendar/EntryCalendar";
9-
import { useEffect } from "react";
9+
import { useEffect, useState } from "react";
1010
import { components } from "../../../../../api.gen";
11-
import { resolveGaps } from "../../../../../utils/entry";
11+
import { GapsPerDay, resolveGaps } from "../../../../../utils/entry";
1212

1313
const messages = i18n("questionnaireEntries", {
1414
formAction: "Continue",
@@ -26,20 +26,6 @@ function QuestionnaireEntries() {
2626

2727
const c = useQueryClient();
2828

29-
const f = useForm<{ entries: components["schemas"]["QuestionnaireEntryDto"][] }>({
30-
initialValues: {
31-
entries: [],
32-
},
33-
validate: {
34-
entries: (value) => {
35-
const gaps = resolveGaps(value);
36-
console.log(gaps);
37-
38-
return !!gaps.length;
39-
},
40-
},
41-
});
42-
4329
const createMutation = $api.useMutation("post", "/entries");
4430
const updateMutation = $api.useMutation("patch", "/entries/{id}");
4531
const deleteMutation = $api.useMutation("delete", "/entries/{id}");
@@ -93,6 +79,22 @@ function QuestionnaireEntries() {
9379
return deleteMutation.mutateAsync({ params: { path: { id: id.toString() } } }, { onSuccess: reloadEntries });
9480
};
9581

82+
const [gaps, setGaps] = useState<GapsPerDay>();
83+
84+
const f = useForm<{ entries: components["schemas"]["QuestionnaireEntryDto"][] }>({
85+
initialValues: {
86+
entries: [],
87+
},
88+
validate: {
89+
entries: (value) => {
90+
const gaps = resolveGaps(value);
91+
setGaps(gaps);
92+
93+
return gaps.some((dailyGaps) => dailyGaps.length);
94+
},
95+
},
96+
});
97+
9698
const handleSubmit = () => {
9799
n({ to: "/questionnaire/$id/remarks", params: p });
98100
};
@@ -107,6 +109,7 @@ function QuestionnaireEntries() {
107109
<Stack>
108110
<EntryCalendar
109111
entries={questionnaire.entries ?? []}
112+
gaps={gaps}
110113
onAddEntry={handleCreate}
111114
onUpdateEntry={handleUpdate}
112115
onDeleteEntry={handleDelete}

apps/frontend/src/utils/entry.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import { components } from "../api.gen";
22

33
type Entry = components["schemas"]["QuestionnaireEntryDto"];
4+
export type Gap = [string, string];
5+
export type GapsPerDay = [Gap[], Gap[], Gap[], Gap[], Gap[], Gap[], Gap[]];
46

57
const groupByWeekday = (entries: Entry[]) =>
68
entries.reduce<Entry[][]>((acc, cur) => {
79
acc[cur.weekday] = [...(acc[cur.weekday] ?? []), cur];
810
return acc;
911
}, []);
1012

11-
export const resolveGaps = (entries: Entry[]) => groupByWeekday(entries).map(resolveGapsInDay);
13+
export const resolveGaps = (entries: Entry[]) => groupByWeekday(entries).map(resolveGapsInDay) as GapsPerDay;
1214

1315
// inspired by: https://cs.stackexchange.com/questions/133276/algorithm-to-compute-the-gaps-between-a-set-of-intervals
1416
const resolveGapsInDay = (entriesOfSameDay: Entry[]) => {
1517
const entriesSortedByStart = entriesOfSameDay.toSorted((a, b) => a.startedAt.localeCompare(b.startedAt));
1618

17-
const gaps: [string, string][] = [];
19+
const gaps: Gap[] = [];
1820
let lastCoveredTime = entriesSortedByStart[0]?.endedAt;
1921

2022
for (const entry of entriesSortedByStart) {

0 commit comments

Comments
 (0)