Skip to content

Commit 1a30ce5

Browse files
committed
feat: implement gap detection algorithm
1 parent 6f84492 commit 1a30ce5

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { Button, Group, Stack, notifications } from "@quassel/ui";
1+
import { Button, Group, Stack, notifications, useForm } from "@quassel/ui";
22
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
33
import { i18n } from "../../../../../stores/i18n";
44
import { useStore } from "@nanostores/react";
55
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";
10+
import { components } from "../../../../../api.gen";
11+
import { resolveGaps } from "../../../../../utils/entry";
912

1013
const messages = i18n("questionnaireEntries", {
1114
formAction: "Continue",
@@ -23,6 +26,20 @@ function QuestionnaireEntries() {
2326

2427
const c = useQueryClient();
2528

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+
2643
const createMutation = $api.useMutation("post", "/entries");
2744
const updateMutation = $api.useMutation("patch", "/entries/{id}");
2845
const deleteMutation = $api.useMutation("delete", "/entries/{id}");
@@ -80,9 +97,13 @@ function QuestionnaireEntries() {
8097
n({ to: "/questionnaire/$id/remarks", params: p });
8198
};
8299

100+
useEffect(() => {
101+
f.setValues({ entries: questionnaire.entries });
102+
}, [questionnaire]);
103+
83104
return (
84105
<>
85-
<form onSubmit={handleSubmit}>
106+
<form onSubmit={f.onSubmit(handleSubmit)}>
86107
<Stack>
87108
<EntryCalendar
88109
entries={questionnaire.entries ?? []}

apps/frontend/src/utils/entry.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { components } from "../api.gen";
2+
3+
type Entry = components["schemas"]["QuestionnaireEntryDto"];
4+
5+
const groupByWeekday = (entries: Entry[]) =>
6+
entries.reduce<Entry[][]>((acc, cur) => {
7+
acc[cur.weekday] = [...(acc[cur.weekday] ?? []), cur];
8+
return acc;
9+
}, []);
10+
11+
export const resolveGaps = (entries: Entry[]) => groupByWeekday(entries).map(resolveGapsInDay);
12+
13+
// inspired by: https://cs.stackexchange.com/questions/133276/algorithm-to-compute-the-gaps-between-a-set-of-intervals
14+
const resolveGapsInDay = (entriesOfSameDay: Entry[]) => {
15+
const entriesSortedByStart = entriesOfSameDay.toSorted((a, b) => a.startedAt.localeCompare(b.startedAt));
16+
17+
const gaps: [string, string][] = [];
18+
let lastCoveredTime = entriesSortedByStart[0]?.endedAt;
19+
20+
for (const entry of entriesSortedByStart) {
21+
if (entry.startedAt > lastCoveredTime) {
22+
gaps.push([lastCoveredTime, entry.startedAt]);
23+
}
24+
lastCoveredTime = lastCoveredTime > entry.endedAt ? lastCoveredTime : entry.endedAt;
25+
}
26+
27+
return gaps;
28+
};

0 commit comments

Comments
 (0)