Skip to content

Commit 0112768

Browse files
committed
feat: allow adding custom carer from within entry dialog
1 parent 725923b commit 0112768

File tree

8 files changed

+126
-26
lines changed

8 files changed

+126
-26
lines changed

apps/backend/src/defaults/carers/carer.dto.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ export class CarerDto {
1919
entries: number[];
2020
}
2121
export class CarerResponseDto extends CarerDto {}
22-
export class CarerCreationDto extends OmitType(CarerDto, ["id", "entries"]) {}
23-
export class CarerMutationDto extends PartialType(CarerCreationDto) {}
22+
export class CarerCreationDto extends OmitType(CarerDto, ["id", "entries", "participant"]) {
23+
participant?: number;
24+
}
25+
export class CarerMutationDto extends PartialType(OmitType(CarerDto, ["entries"])) {}

apps/frontend/src/api.gen.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,14 @@ export interface components {
452452
*/
453453
role?: "ASSISTANT" | "ADMIN";
454454
};
455+
CarerCreationDto: {
456+
/**
457+
* @description The name of the carer
458+
* @example Grandmother
459+
*/
460+
name: string;
461+
participant?: number;
462+
};
455463
ParticipantDto: {
456464
/**
457465
* @description The id of the participant (child id)
@@ -468,14 +476,6 @@ export interface components {
468476
carers: number[];
469477
languages: number[];
470478
};
471-
CarerCreationDto: {
472-
/**
473-
* @description The name of the carer
474-
* @example Grandmother
475-
*/
476-
name: string;
477-
participant?: components["schemas"]["ParticipantDto"];
478-
};
479479
CarerResponseDto: {
480480
/**
481481
* @description The id of the carer
@@ -491,6 +491,11 @@ export interface components {
491491
entries: number[];
492492
};
493493
CarerMutationDto: {
494+
/**
495+
* @description The id of the carer
496+
* @example 1
497+
*/
498+
id?: number;
494499
/**
495500
* @description The name of the carer
496501
* @example Grandmother

apps/frontend/src/components/CarerSelect.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { EntitySelect, EntitySelectProps } from "./EntitySelect";
33

44
type CarerSelectProps = EntitySelectProps;
55

6-
export function CarerSelect({ value, onChange, ...rest }: CarerSelectProps) {
6+
export function CarerSelect({ value, onChange, onAddNew, ...rest }: CarerSelectProps) {
77
const { data } = $api.useQuery("get", "/carers");
88

9-
return <EntitySelect value={value} onChange={onChange} {...rest} data={data} buildLabel={(carer) => carer.name} />;
9+
return <EntitySelect value={value} onChange={onChange} onAddNew={onAddNew} {...rest} data={data} inputKey="name" />;
1010
}
Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,99 @@
1-
import { Select, SelectProps } from "@quassel/ui";
1+
import { Combobox, TextInput, TextInputProps, useCombobox } from "@quassel/ui";
2+
import { useEffect, useState } from "react";
3+
import { i18n } from "../stores/i18n";
4+
import { useStore } from "@nanostores/react";
5+
import { params } from "@nanostores/i18n";
26

3-
export type EntitySelectProps = Omit<SelectProps, "value" | "onChange"> & {
7+
export type EntitySelectProps = Omit<TextInputProps, "value" | "onChange"> & {
48
value?: number;
5-
onChange?: (id?: number) => void;
9+
onChange?: (value?: number) => void;
10+
onAddNew?: (value: string) => Promise<number>;
611
};
712

13+
type StringKeys<T> = { [K in keyof T]-?: T[K] extends string ? K : never }[keyof T];
14+
815
type Props<T extends { id: number }> = Omit<EntitySelectProps, "data"> & {
916
data?: T[];
10-
buildLabel: (value: T) => string;
17+
onAddNew?: (value: string) => void;
18+
inputKey: StringKeys<T>;
1119
};
1220

13-
export function EntitySelect<T extends { id: number }>({ value, onChange, data, buildLabel, ...rest }: Props<T>) {
21+
const customValueKey = "CUSTOM_VALUE";
22+
23+
const messages = i18n("entitySelect", {
24+
actionCreateNew: params('Create new "{value}"'),
25+
});
26+
27+
export function EntitySelect<T extends { id: number }>({ value, onChange, data, inputKey, onAddNew, ...rest }: Props<T>) {
28+
const t = useStore(messages);
29+
30+
const combobox = useCombobox({
31+
onDropdownClose: () => combobox.resetSelectedOption(),
32+
});
33+
34+
const [searchValue, setSearchValue] = useState("");
35+
36+
const shouldFilterOptions = !data?.some((item) => item[inputKey] === searchValue);
37+
const filteredOptions =
38+
(shouldFilterOptions
39+
? data?.filter((item) => (item[inputKey] as string)?.toLowerCase().includes(searchValue.toLowerCase().trim()))
40+
: data) ?? [];
41+
42+
const options = filteredOptions?.map((item) => (
43+
<Combobox.Option key={item.id} value={item.id.toString()}>
44+
{item[inputKey] as string}
45+
</Combobox.Option>
46+
));
47+
48+
useEffect(() => {
49+
if (value && data) {
50+
const index = data.findIndex((item) => item.id === value);
51+
if (index !== -1) {
52+
setSearchValue(data[index][inputKey] as string);
53+
combobox.selectOption(index);
54+
}
55+
}
56+
}, [value, data]);
57+
58+
useEffect(() => {
59+
if (shouldFilterOptions) combobox.selectFirstOption();
60+
}, [searchValue]);
61+
1462
return (
15-
<Select
16-
value={value?.toString()}
17-
onChange={(value) => onChange?.(value ? parseInt(value) : undefined)}
18-
data={data?.map((entity) => ({ value: entity.id.toString(), label: buildLabel(entity) })) ?? []}
19-
{...rest}
20-
/>
63+
<Combobox
64+
store={combobox}
65+
onOptionSubmit={async (value) => {
66+
let id: number;
67+
if (value === customValueKey) {
68+
id = await onAddNew!(searchValue);
69+
} else {
70+
id = parseInt(value);
71+
}
72+
onChange?.(id);
73+
combobox.closeDropdown();
74+
}}
75+
>
76+
<Combobox.Target>
77+
<TextInput
78+
value={searchValue}
79+
onChange={({ target: { value } }) => {
80+
setSearchValue(value);
81+
}}
82+
onClick={() => combobox.openDropdown()}
83+
onFocus={() => combobox.openDropdown()}
84+
onBlur={() => combobox.closeDropdown()}
85+
{...rest}
86+
/>
87+
</Combobox.Target>
88+
89+
<Combobox.Dropdown>
90+
<Combobox.Options>
91+
{onAddNew && !options?.length && (
92+
<Combobox.Option value={customValueKey}>{t.actionCreateNew({ value: searchValue })}</Combobox.Option>
93+
)}
94+
{options}
95+
</Combobox.Options>
96+
</Combobox.Dropdown>
97+
</Combobox>
2198
);
2299
}

apps/frontend/src/components/LanguageSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ type LanguageSelectProps = EntitySelectProps;
66
export function LanguageSelect({ value, onChange, ...rest }: LanguageSelectProps) {
77
const { data } = $api.useQuery("get", "/languages");
88

9-
return <EntitySelect value={value} onChange={onChange} searchable {...rest} data={data} buildLabel={(language) => language.name} />;
9+
return <EntitySelect value={value} onChange={onChange} {...rest} data={data} inputKey="name" />;
1010
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ const messages = i18n("entityForm", {
3030
type EntityFormProps = {
3131
onSave: (entity: EntryFormValues) => void;
3232
onDelete?: () => void;
33+
onAddCarer: (value: string) => Promise<number>;
3334
entry?: Partial<EntryFormValues>;
3435
actionLabel: string;
3536
};
3637

37-
export function EntityForm({ onSave, onDelete, actionLabel, entry }: EntityFormProps) {
38+
export function EntityForm({ onSave, onDelete, onAddCarer, actionLabel, entry }: EntityFormProps) {
3839
const t = useStore(messages);
3940
const f = useForm<EntryFormValues>({
4041
initialValues: {
@@ -89,7 +90,7 @@ export function EntityForm({ onSave, onDelete, actionLabel, entry }: EntityFormP
8990
return (
9091
<form onSubmit={f.onSubmit(onSave)}>
9192
<Stack>
92-
<CarerSelect {...f.getInputProps("carer")} placeholder={t.labelCarer} />
93+
<CarerSelect {...f.getInputProps("carer")} onAddNew={onAddCarer} placeholder={t.labelCarer} />
9394

9495
{f.getValues().entryLanguages.map((_, index) => (
9596
// TODO: make key either languageId or name of new language entry

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { components } from "../../../../../api.gen";
1111
import { QuestionnaireEntry } from "../../../../../components/questionnaire/calendar/QuestionnaireEntry";
1212
import { EntityForm, EntryFormValues } from "../../../../../components/questionnaire/calendar/EntryForm";
1313
import { useState } from "react";
14+
import { useQueryClient } from "@tanstack/react-query";
1415

1516
export type ExtendedEvent = EventInput & { extendedProps: { entryLanguages: components["schemas"]["EntryLanguageResponseDto"][] } };
1617

@@ -38,6 +39,8 @@ function QuestionnaireEntries() {
3839

3940
const t = useStore(messages);
4041

42+
const c = useQueryClient();
43+
4144
const theme = useMantineTheme();
4245
const [opened, { open, close }] = useDisclosure();
4346

@@ -50,6 +53,12 @@ function QuestionnaireEntries() {
5053
const deleteMutation = $api.useMutation("delete", "/entries/{id}");
5154
const { data: questionnaire, refetch } = $api.useSuspenseQuery("get", "/questionnaires/{id}", { params: { path: { id: p.id } } });
5255

56+
const createCarerMutation = $api.useMutation("post", "/carers", {
57+
onSuccess() {
58+
c.refetchQueries($api.queryOptions("get", "/carers"));
59+
},
60+
});
61+
5362
const events: ExtendedEvent[] =
5463
questionnaire.entries?.map(({ startedAt, endedAt, weekday, carer, entryLanguages, id }) => ({
5564
id: id.toString(),
@@ -111,6 +120,9 @@ function QuestionnaireEntries() {
111120
<>
112121
<Modal opened={opened} onClose={close} size="md">
113122
<EntityForm
123+
onAddCarer={(name) =>
124+
createCarerMutation.mutateAsync({ body: { name, participant: questionnaire?.participant?.id } }).then(({ id }) => id)
125+
}
114126
onSave={handleOnSave}
115127
onDelete={entryUpdatingId ? () => handleDelete(entryUpdatingId) : undefined}
116128
entry={entryDraft}

libs/ui/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export {
4343
AppShell,
4444
Button,
4545
Checkbox,
46+
Combobox,
4647
Container,
4748
Divider,
4849
Flex,
@@ -57,9 +58,11 @@ export {
5758
Table,
5859
Text,
5960
TextInput,
61+
type TextInputProps,
6062
NumberInput,
6163
Title,
6264
UnstyledButton,
65+
useCombobox,
6366
useMantineTheme,
6467
} from "@mantine/core";
6568

0 commit comments

Comments
 (0)