Skip to content

Commit 4281239

Browse files
committed
Merge branch 'main' into 22-design-data-export-interface-format-selection-filtering
2 parents 8d90ed5 + 771672f commit 4281239

File tree

21 files changed

+311
-88
lines changed

21 files changed

+311
-88
lines changed

.changeset/dirty-knives-appear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@quassel/frontend": patch
3+
---
4+
5+
Add validation and highlighting for gaps in calendar

.changeset/loud-parents-press.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@quassel/frontend": patch
3+
"@quassel/backend": patch
4+
"@quassel/mockup": patch
5+
"@quassel/ui": patch
6+
---
7+
8+
Rebrand quassel to LEMON

apps/backend/db/seeds/DatabaseSeeder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ export class DatabaseSeeder extends Seeder {
1818
{
1919
role: UserRole.ADMIN,
2020
email: "admin@example.com",
21-
password: await getPasswordHash("Quassel*1234"),
21+
password: await getPasswordHash("Lemon*1234"),
2222
},
2323
{
2424
role: UserRole.ASSISTANT,
2525
email: "assistant@example.com",
26-
password: await getPasswordHash("Quassel*1234"),
26+
password: await getPasswordHash("Lemon*1234"),
2727
},
2828
];
2929

apps/frontend/index.html

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<title>Quassel</title>
7-
<link rel="icon" href="/favicon.ico" />
8-
<style>
9-
html {
10-
height: 100%;
11-
}
123

13-
body {
14-
margin: 0;
15-
padding: 0;
16-
min-height: 100%;
17-
}
4+
<head>
5+
<meta charset="UTF-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Lemon</title>
8+
<link rel="icon" href="/favicon.ico" />
9+
<style>
10+
html {
11+
height: 100%;
12+
}
1813

19-
#root {
20-
min-height: 100%;
21-
}
22-
</style>
23-
<script type="text/javascript">
24-
window.env = {
25-
apiUrl: '//REPLACE_WITH_API_URL'
26-
};
27-
</script>
28-
</head>
29-
<body>
30-
<div id="root"></div>
31-
<script type="module" src="/src/main.tsx"></script>
32-
</body>
33-
</html>
14+
body {
15+
margin: 0;
16+
padding: 0;
17+
min-height: 100%;
18+
}
19+
20+
#root {
21+
min-height: 100%;
22+
}
23+
</style>
24+
<script type="text/javascript">
25+
window.env = {
26+
apiUrl: '//REPLACE_WITH_API_URL'
27+
};
28+
</script>
29+
</head>
30+
31+
<body>
32+
<div id="root"></div>
33+
<script type="module" src="/src/main.tsx"></script>
34+
</body>
35+
36+
</html>

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

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import FullCalendar from "@fullcalendar/react";
22
import interactionPlugin from "@fullcalendar/interaction";
33
import timeGridPlugin from "@fullcalendar/timegrid";
4-
import { EventChangeArg, EventInput } from "@fullcalendar/core";
4+
import { DateSelectArg, EventChangeArg, EventInput } from "@fullcalendar/core";
55
import { formatDate, getDateFromTimeAndWeekday, getTime, isSame, Modal, useDisclosure, useMantineTheme } from "@quassel/ui";
66
import { QuestionnaireEntry } from "./QuestionnaireEntry";
77
import { components } from "../../../api.gen";
88
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";
13+
import { EventImpl } from "@fullcalendar/core/internal";
1214

1315
const calendarBaseConfig: FullCalendar["props"] = {
1416
allDaySlot: false,
@@ -27,10 +29,13 @@ const calendarBaseConfig: FullCalendar["props"] = {
2729
eventLongPressDelay: 400,
2830
};
2931

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

3236
export type EntryCalendarProps = {
3337
entries: components["schemas"]["QuestionnaireEntryDto"][];
38+
gaps?: GapsPerDay;
3439
onAddEntry: (entry: EntryFormValues, weekday: number) => Promise<unknown>;
3540
onUpdateEntry: (id: number, entry: Partial<EntryFormValues>, weekday: number) => Promise<unknown>;
3641
onDeleteEntry: (id: number) => Promise<unknown>;
@@ -47,6 +52,7 @@ const messages = i18n("entryCalendar", {
4752

4853
export function EntryCalendar({
4954
entries,
55+
gaps,
5056
onAddEntry,
5157
onUpdateEntry,
5258
onDeleteEntry,
@@ -69,19 +75,48 @@ export function EntryCalendar({
6975

7076
useEffect(() => {
7177
if (entries) {
72-
setEvents(
73-
entries.map(({ startedAt, endedAt, weekday, carer, entryLanguages, id }) => ({
78+
setEvents([
79+
...entries.map(({ startedAt, endedAt, weekday, carer, weeklyRecurring, entryLanguages, id }) => ({
7480
id: id.toString(),
7581
start: getDateFromTimeAndWeekday(startedAt, weekday),
7682
end: getDateFromTimeAndWeekday(endedAt, weekday),
7783
title: carer.name,
78-
extendedProps: { entryLanguages },
84+
extendedProps: { entryLanguages, weeklyRecurring },
7985
backgroundColor: carer.color ?? theme.colors[theme.primaryColor][4],
8086
borderColor: carer.color ?? theme.colors[theme.primaryColor][4],
81-
})) ?? []
82-
);
87+
})),
88+
...(gaps ?? []).flatMap((dailyGaps, index) =>
89+
dailyGaps.map((gap) => ({
90+
start: getDateFromTimeAndWeekday(gap[0], index),
91+
end: getDateFromTimeAndWeekday(gap[1], index),
92+
backgroundColor: theme.colors.uzhBerry[4],
93+
display: "background",
94+
}))
95+
),
96+
]);
8397
}
84-
}, [entries]);
98+
}, [entries, gaps]);
99+
100+
const setupEntryUpdate = (event: EventImpl) => {
101+
const { carer, entryLanguages, id, weekday, ...rest } = entries?.find((entry) => entry.id.toString() === event.id) ?? {};
102+
103+
setEntryDraft({
104+
carer: carer?.id,
105+
entryLanguages: entryLanguages?.map(({ language, ...rest }) => ({ ...rest, language: language.id })),
106+
...rest,
107+
startedAt: getTime(event.start!),
108+
endedAt: getTime(event.end!),
109+
});
110+
setSelectedWeekday(weekday);
111+
setEntryUpdadingId(id);
112+
open();
113+
};
114+
115+
const setupEntryCreate = ({ start, end }: DateSelectArg | EventImpl) => {
116+
setEntryDraft({ startedAt: getTime(start!), endedAt: getTime(end!) });
117+
setSelectedWeekday(start!.getDay());
118+
open();
119+
};
85120

86121
const handleEventChange = ({ event }: EventChangeArg) => {
87122
const { id, start, end } = event;
@@ -119,24 +154,13 @@ export function EntryCalendar({
119154
{...calendarBaseConfig}
120155
plugins={[timeGridPlugin, interactionPlugin]}
121156
events={events}
122-
select={({ start, end }) => {
123-
setEntryDraft({ startedAt: getTime(start), endedAt: getTime(end) });
124-
setSelectedWeekday(start.getDay());
125-
open();
126-
}}
157+
select={setupEntryCreate}
127158
eventClick={({ event }) => {
128-
const { carer, entryLanguages, id, weekday, ...rest } = entries?.find((entry) => entry.id.toString() === event.id) ?? {};
129-
130-
setEntryDraft({
131-
carer: carer?.id,
132-
entryLanguages: entryLanguages?.map(({ language, ...rest }) => ({ ...rest, language: language.id })),
133-
...rest,
134-
startedAt: getTime(event.start!),
135-
endedAt: getTime(event.end!),
136-
});
137-
setSelectedWeekday(weekday);
138-
setEntryUpdadingId(id);
139-
open();
159+
if (event.display === "background") {
160+
setupEntryCreate(event);
161+
} else {
162+
setupEntryUpdate(event);
163+
}
140164
}}
141165
eventChange={handleEventChange}
142166
eventContent={({ event }) => <QuestionnaireEntry event={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/__root.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
IconCalendarWeek,
1616
IconMapSearch,
1717
Divider,
18+
FooterLogos,
1819
} from "@quassel/ui";
1920
import { createRootRouteWithContext, Link, Outlet, RouteContext, useNavigate } from "@tanstack/react-router";
2021
import { version } from "../../package.json";
@@ -53,7 +54,7 @@ function Root() {
5354
return (
5455
<>
5556
<AppShell
56-
header={{ height: 118 }}
57+
header={{ height: 104 }}
5758
footer={{ height: 84 }}
5859
navbar={{ width: 300, breakpoint: "sm", collapsed: { desktop: !layoutStore.admin } }}
5960
padding="xl"
@@ -67,7 +68,7 @@ function Root() {
6768
{sessionStore.email && (
6869
<Group>
6970
<Text>{sessionStore.email}</Text>
70-
<Button leftSection={<IconLogout />} onClick={handleSignOut}>
71+
<Button variant="outline" leftSection={<IconLogout />} onClick={handleSignOut}>
7172
Sign out
7273
</Button>
7374
</Group>
@@ -95,10 +96,16 @@ function Root() {
9596
<NavLink component={Link} to="/administration/users" leftSection={<IconUsers />} label="Users" />
9697
</AppShell.Navbar>
9798
)}
99+
98100
<AppShell.Main>
99101
<Outlet />
100102
</AppShell.Main>
101-
<AppShell.Footer>Version {version}</AppShell.Footer>
103+
<AppShell.Footer>
104+
<Group justify="space-between">
105+
<FooterLogos />
106+
Version {version}
107+
</Group>
108+
</AppShell.Footer>
102109
</AppShell>
103110
</>
104111
);

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

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
import { Button, Group, Stack, notifications } from "@quassel/ui";
1+
import { Button, Group, Modal, Stack, notifications, useDisclosure, 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, useState } from "react";
10+
import { components } from "../../../../../api.gen";
11+
import { GapsPerDay, resolveGaps } from "../../../../../utils/entry";
912

1013
const messages = i18n("questionnaireEntries", {
1114
formAction: "Continue",
1215
backAction: "Back",
1316
addEntityLabel: "Add",
1417
notificationSuccessCreateLanguage: "Successfully add a new language.",
1518
notificationSuccessCreateCarer: "Successfully add a new carer.",
19+
gapsDialogTitle: "Gaps detected in the calendar",
20+
gapsDialogContinueAnyway: "Continue anyway",
21+
gapsDialogHighlightGaps: "Highlight gaps",
1622
});
1723

1824
function QuestionnaireEntries() {
@@ -76,16 +82,57 @@ function QuestionnaireEntries() {
7682
return deleteMutation.mutateAsync({ params: { path: { id: id.toString() } } }, { onSuccess: reloadEntries });
7783
};
7884

85+
const [gaps, setGaps] = useState<GapsPerDay>();
86+
const [highlightGaps, setHighlightGaps] = useState(false);
87+
const [gapsDialogOpened, { open, close }] = useDisclosure();
88+
89+
const f = useForm<{ entries: components["schemas"]["QuestionnaireEntryDto"][] }>({
90+
initialValues: {
91+
entries: [],
92+
},
93+
validate: {
94+
entries: (value) => {
95+
const gaps = resolveGaps(value);
96+
setGaps(gaps);
97+
98+
const hasGaps = gaps.some(({ length }) => length);
99+
if (hasGaps) open();
100+
101+
return hasGaps;
102+
},
103+
},
104+
});
105+
79106
const handleSubmit = () => {
80107
n({ to: "/questionnaire/$id/remarks", params: p });
81108
};
82109

110+
useEffect(() => {
111+
f.setValues({ entries: questionnaire.entries });
112+
}, [questionnaire]);
113+
83114
return (
84115
<>
85-
<form onSubmit={handleSubmit}>
116+
<form onSubmit={f.onSubmit(handleSubmit)}>
86117
<Stack>
118+
<Modal opened={gapsDialogOpened} onClose={close} centered title={t.gapsDialogTitle}>
119+
<Group justify="flex-end">
120+
<Button onClick={handleSubmit} variant="light" type="submit">
121+
{t.gapsDialogContinueAnyway}
122+
</Button>
123+
<Button
124+
onClick={() => {
125+
setHighlightGaps(true);
126+
close();
127+
}}
128+
>
129+
{t.gapsDialogHighlightGaps}
130+
</Button>
131+
</Group>
132+
</Modal>
87133
<EntryCalendar
88134
entries={questionnaire.entries ?? []}
135+
gaps={highlightGaps ? gaps : undefined}
89136
onAddEntry={handleCreate}
90137
onUpdateEntry={handleUpdate}
91138
onDeleteEntry={handleDelete}

apps/frontend/src/sections/HeroSection.module.css

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,11 @@
1515
font-size: rem(44px);
1616
line-height: 1.2;
1717
font-weight: 900;
18-
19-
}
20-
21-
.control {
2218
}
2319

2420
.highlight {
2521
position: relative;
26-
background-color: var(--mantine-color-blue-light);
22+
background-color: var(--mantine-primary-color-light);
2723
border-radius: var(--mantine-radius-sm);
2824
padding: rem(4px) rem(12px);
2925
}

apps/frontend/src/sections/HeroSection.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)