From 372b7d69485d7b8f71749c6ae7fd220287740324 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 7 Feb 2024 11:48:57 +0100 Subject: [PATCH 1/8] fix: improve login/logout Allow login/logout to work without Javascript --- src/hooks.server.ts | 1 + src/lib/server/trpc/index.ts | 13 +------------ src/lib/shared/util/index.ts | 6 ++++++ src/routes/(app)/+layout.svelte | 8 +------- src/routes/(app)/logout/+page.svelte | 23 +++++++++++++++++++++++ src/routes/(app)/logout/+page.ts | 8 ++++++++ src/routes/login/+page.svelte | 24 +++++++++++++++++------- src/routes/login/+page.ts | 8 ++++++++ 8 files changed, 65 insertions(+), 26 deletions(-) create mode 100644 src/routes/(app)/logout/+page.svelte create mode 100644 src/routes/(app)/logout/+page.ts create mode 100644 src/routes/login/+page.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 065efc2..3e27cfd 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -65,6 +65,7 @@ export const handle = sequence( return opt.session; }, }, + trustHost: true, }), authorization, diff --git a/src/lib/server/trpc/index.ts b/src/lib/server/trpc/index.ts index 5007f37..877310d 100644 --- a/src/lib/server/trpc/index.ts +++ b/src/lib/server/trpc/index.ts @@ -3,15 +3,4 @@ import type { Context } from "./context"; export { trpcWrap } from "$lib/server/trpc/handleError"; -export const t = initTRPC.context().create({ - errorFormatter(opts) { - const { shape, error } = opts; - console.log("formatted", error); - return { - ...shape, - data: { - ...shape.data, - }, - }; - }, -}); +export const t = initTRPC.context().create(); diff --git a/src/lib/shared/util/index.ts b/src/lib/shared/util/index.ts index 76543f7..95dcb50 100644 --- a/src/lib/shared/util/index.ts +++ b/src/lib/shared/util/index.ts @@ -62,3 +62,9 @@ export async function loadWrap(f: () => Promise) { } } } + +export async function authjsCsrfToken(f: typeof fetch): Promise { + const csrfResp = await f("/auth/csrf"); + const csrfJson = await csrfResp.json(); + return csrfJson.csrfToken; +} diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 6237525..96dade7 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -42,13 +42,7 @@
  • Zimmer
  • Kategorien
  • Benutzer
  • -
  • - -
  • +
  • Abmelden
  • {/if} diff --git a/src/routes/(app)/logout/+page.svelte b/src/routes/(app)/logout/+page.svelte new file mode 100644 index 0000000..3ce5200 --- /dev/null +++ b/src/routes/(app)/logout/+page.svelte @@ -0,0 +1,23 @@ + + +
    +

    Möchten sie sich abmelden?

    +
    + + + +
    +
    diff --git a/src/routes/(app)/logout/+page.ts b/src/routes/(app)/logout/+page.ts new file mode 100644 index 0000000..6f08ac0 --- /dev/null +++ b/src/routes/(app)/logout/+page.ts @@ -0,0 +1,8 @@ +import { authjsCsrfToken } from "$lib/shared/util"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ fetch }) => { + const csrfToken = await authjsCsrfToken(fetch); + + return { csrfToken }; +}; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index e980f27..8b4fc97 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -1,11 +1,20 @@
    @@ -19,8 +20,10 @@ - - + {#if !perPatient} + + + {/if} @@ -44,10 +47,12 @@ aria-label="Eintrag anzeigen">{entry.id} - - {#if entry.patient.room}{/if} + {#if !perPatient} + + {#if entry.patient.room}{/if} + {/if} {#if entry.current_version.category} diff --git a/src/lib/components/table/FilteredEntryTable.svelte b/src/lib/components/table/FilteredEntryTable.svelte new file mode 100644 index 0000000..db3c6aa --- /dev/null +++ b/src/lib/components/table/FilteredEntryTable.svelte @@ -0,0 +1,103 @@ + + + + + + +{#if loadError} + +{:else} + + + +{/if} diff --git a/src/lib/components/table/PatientTable.svelte b/src/lib/components/table/PatientTable.svelte index 042e008..40f97eb 100644 --- a/src/lib/components/table/PatientTable.svelte +++ b/src/lib/components/table/PatientTable.svelte @@ -3,7 +3,7 @@ import type { RouterOutput } from "$lib/shared/trpc"; import { formatDate } from "$lib/shared/util"; import { mdiClose } from "@mdi/js"; - import Icon from "../ui/Icon.svelte"; + import Icon from "$lib/components/ui/Icon.svelte"; import RoomField from "./RoomField.svelte"; import SortHeader from "./SortHeader.svelte"; diff --git a/src/lib/components/ui/EditableField.svelte b/src/lib/components/ui/EditableField.svelte new file mode 100644 index 0000000..aac679a --- /dev/null +++ b/src/lib/components/ui/EditableField.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/lib/server/trpc/routes/entry.ts b/src/lib/server/trpc/routes/entry.ts index 5b1487d..6086eaf 100644 --- a/src/lib/server/trpc/routes/entry.ts +++ b/src/lib/server/trpc/routes/entry.ts @@ -3,12 +3,10 @@ import { z } from "zod"; import { ZEntityId, - ZEntriesFilter, + ZEntriesQuery, ZEntryExecutionNew, ZEntryNew, ZEntryVersionNew, - ZPagination, - ZSort, } from "$lib/shared/model/validation"; import { getEntries, @@ -25,15 +23,7 @@ export const entryRouter = t.router({ .input(ZEntityId) .query(async (opts) => trpcWrap(async () => getEntry(opts.input))), list: t.procedure - .input( - z - .object({ - filter: ZEntriesFilter, - pagination: ZPagination, - sort: ZSort, - }) - .partial() - ) + .input(ZEntriesQuery) .query(async (opts) => trpcWrap(async () => getEntries( diff --git a/src/lib/server/trpc/routes/patient.ts b/src/lib/server/trpc/routes/patient.ts index af3139d..575e8d2 100644 --- a/src/lib/server/trpc/routes/patient.ts +++ b/src/lib/server/trpc/routes/patient.ts @@ -6,13 +6,7 @@ import { hidePatient, updatePatient, } from "$lib/server/query"; -import { - ZEntityId, - ZPagination, - ZPatientNew, - ZPatientsFilter, - ZSort, -} from "$lib/shared/model/validation"; +import { ZEntityId, ZPatientNew, ZPatientsQuery } from "$lib/shared/model/validation"; import { t, trpcWrap } from ".."; import { z } from "zod"; @@ -21,19 +15,13 @@ export const patientRouter = t.router({ get: t.procedure .input(ZEntityId) .query(async (opts) => trpcWrap(async () => getPatient(opts.input))), - list: t.procedure - .input( - z - .object({ filter: ZPatientsFilter, pagination: ZPagination, sort: ZSort }) - .partial() - ) - .query(async (opts) => { - return getPatients( - opts.input.filter ?? {}, - opts.input.pagination ?? {}, - opts.input.sort ?? {} - ); - }), + list: t.procedure.input(ZPatientsQuery).query(async (opts) => { + return getPatients( + opts.input.filter ?? {}, + opts.input.pagination ?? {}, + opts.input.sort ?? {} + ); + }), update: t.procedure .input(z.object({ id: ZEntityId, patient: ZPatientNew.partial() })) .mutation(async (opts) => diff --git a/src/lib/shared/model/model.ts b/src/lib/shared/model/model.ts index b9b7726..4c51cf6 100644 --- a/src/lib/shared/model/model.ts +++ b/src/lib/shared/model/model.ts @@ -81,7 +81,7 @@ export type PatientNew = { first_name: string; last_name: string; age: Option; - room_id: number; + room_id?: number; }; export type Entry = { diff --git a/src/lib/shared/model/validation.ts b/src/lib/shared/model/validation.ts index 75d17af..6c30389 100644 --- a/src/lib/shared/model/validation.ts +++ b/src/lib/shared/model/validation.ts @@ -46,7 +46,7 @@ export const ZPatientNew = implement().with({ first_name: ZNameString, last_name: ZNameString, age: z.number().int().nonnegative().lt(200).nullable(), - room_id: ZEntityId, + room_id: ZEntityId.optional(), }); export const ZEntryVersionNew = implement().with({ diff --git a/src/routes/(app)/patient/[id]/[[query]]/+page.server.ts b/src/routes/(app)/patient/[id]/[[query]]/+page.server.ts new file mode 100644 index 0000000..4e2f2d0 --- /dev/null +++ b/src/routes/(app)/patient/[id]/[[query]]/+page.server.ts @@ -0,0 +1,22 @@ +import { ZUrlEntityId } from "$lib/shared/model/validation"; +import { trpc } from "$lib/shared/trpc"; +import { loadWrap } from "$lib/shared/util"; +import type { Actions } from "./$types"; + +export const actions = { + default: async (event) => { + const id = ZUrlEntityId.parse(event.params.id); + const form = await event.request.formData(); + await loadWrap(async () => + trpc(event).patient.update.mutate({ + id, + patient: { + first_name: form.get("first_name")?.toString(), + last_name: form.get("last_name")?.toString(), + age: Number(form.get("age")?.toString()), + // room_id: Number(form.get("room")?.toString()), + }, + }) + ); + }, +} satisfies Actions; diff --git a/src/routes/(app)/patient/[id]/[[query]]/+page.svelte b/src/routes/(app)/patient/[id]/[[query]]/+page.svelte new file mode 100644 index 0000000..12d889b --- /dev/null +++ b/src/routes/(app)/patient/[id]/[[query]]/+page.svelte @@ -0,0 +1,67 @@ + + + + Patient #{data.patient.id} + + +

    + Patient #{data.patient.id} +

    + +
    { + return ({ update }) => { + update({ reset: false }); + }; + }} +> +
    + + + +
    + +
    + + + +
    + + +
    + +

    Einträge

    + + diff --git a/src/routes/(app)/patient/[id]/[[query]]/+page.ts b/src/routes/(app)/patient/[id]/[[query]]/+page.ts new file mode 100644 index 0000000..4c1e31c --- /dev/null +++ b/src/routes/(app)/patient/[id]/[[query]]/+page.ts @@ -0,0 +1,23 @@ +import { ZUrlEntityId } from "$lib/shared/model/validation"; +import { trpc } from "$lib/shared/trpc"; +import { loadWrap } from "$lib/shared/util"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async (event) => { + const id = ZUrlEntityId.parse(event.params.id); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let query: any = {}; + if (event.params.query) { + query = JSON.parse(event.params.query); + } + if (!query.filter) query.filter = {}; + query.filter.patient = [{ id }]; + + const [patient, entries] = await Promise.all([ + loadWrap(async () => trpc(event).patient.get.query(id)), + loadWrap(() => trpc(event).entry.list.query(query)), + ]); + + return { patient, query, entries }; +}; diff --git a/src/routes/(app)/plan/[[query]]/+page.svelte b/src/routes/(app)/plan/[[query]]/+page.svelte index c9fa6bc..c4b3a76 100644 --- a/src/routes/(app)/plan/[[query]]/+page.svelte +++ b/src/routes/(app)/plan/[[query]]/+page.svelte @@ -1,90 +1,13 @@ Planung - - - - -{#if error} - -{:else} - - - -{/if} + From 41d098cc900749c2a103af1aefa6be60e2737039 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 9 Feb 2024 17:32:41 +0100 Subject: [PATCH 3/8] feat: add patient edit form --- src/lib/components/filter/Autocomplete.svelte | 75 ++++++++++++------- src/lib/components/filter/FilterBar.svelte | 31 ++++---- src/lib/components/filter/FilterChip.svelte | 2 +- .../patient/[id]/[[query]]/+page.server.ts | 2 +- .../(app)/patient/[id]/[[query]]/+page.svelte | 12 ++- 5 files changed, 81 insertions(+), 41 deletions(-) diff --git a/src/lib/components/filter/Autocomplete.svelte b/src/lib/components/filter/Autocomplete.svelte index 4c0c8a6..c549cd1 100644 --- a/src/lib/components/filter/Autocomplete.svelte +++ b/src/lib/components/filter/Autocomplete.svelte @@ -16,7 +16,7 @@ type OnSelectResult = { newValue: string; close: boolean }; export let items: BaseItem[] | (() => Promise); - export let defaultSelection: SelectionOrText | null = null; + export let selection: SelectionOrText | null = null; export let hiddenIds: Set = new Set(); export let cache: { [key: string]: BaseItem[] } = {}; export let cacheKey: string | undefined = undefined; @@ -24,13 +24,15 @@ export let padding = true; export let cls = ""; export let inputCls = "w-full bg-transparent"; + export let asTextInput = false; + export let idInputName: string | undefined = undefined; /** Selection callback. Returns the new input value after selection */ export let onSelect: (item: SelectionOrText, kb: boolean) => OnSelectResult = ( item, kb ) => { - return { newValue: item.name || "", close: true }; + return { newValue: item.name ?? "", close: true }; }; export let onClose = (kb: boolean) => {}; export let onBackspace = () => {}; @@ -41,7 +43,7 @@ let srcItems: BaseItem[]; let filteredItems: BaseItem[] = []; - $: if (hiddenIds) updateSearch(); + $: if (hiddenIds || selection) updateSearch(); // HTML elements let inputElm: HTMLInputElement | undefined; @@ -71,7 +73,7 @@ srcItems = items; if (cacheKey) cache[cacheKey] = items; isLoading = false; - onInput(); + updateSearch(); }); return false; } @@ -79,38 +81,49 @@ return true; } - function selectDefault() { - if (defaultSelection) { - if (defaultSelection.id) { - const i = srcItems.findIndex((itm) => itm.id === defaultSelection?.id); + function markSelection() { + if (selection) { + if (selection.id) { + const i = srcItems.findIndex((itm) => itm.id === selection?.id); if (i !== -1) { highlightIndex = i; } + if (asTextInput) setInputValue(selection.name || ""); } else { - setInputValue(defaultSelection.name || ""); highlightIndex = 0; + setInputValue(selection.name || ""); } } else { highlightIndex = 0; } + highlight(); + } + + function clearSelection() { + selection = null; + setInputValue(""); + highlightIndex = 0; + updateSearch(); } function onInput() { - updateSearch(); + selection = null; opened = true; + updateSearch(); } function updateSearch() { + console.log("usel", selection); if (loadSrcItems()) { let searchWord = inputValue().toLowerCase().trim(); filteredItems = - searchWord.length > 0 + !selection && searchWord.length > 0 ? srcItems.filter( (it) => !hiddenIds.has(it.id) && it.name.toLowerCase().includes(searchWord) ) : srcItems.filter((it) => !hiddenIds.has(it.id)); - selectDefault(); + markSelection(); } } @@ -130,6 +143,7 @@ } function selectListItem(item: BaseItem | undefined, kb: boolean) { + selection = { id: item?.id, name: item?.name }; const selRes = onSelect(item ?? { name: inputValue() }, kb); setInputValue(selRes.newValue); if (selRes.close) { @@ -169,6 +183,8 @@ Backspace: () => { if (inputValue().length === 0) { onBackspace(); + } else if (selection) { + clearSelection(); } }, }; @@ -189,20 +205,22 @@ } function highlight() { - window.setTimeout(() => { - const query = ".selected"; + if (browser && opened) { + window.setTimeout(() => { + const query = ".selected"; - const el = listElm && listElm.querySelector(query); - if (el) { - // @ts-expect-error scrollIntoViewIfNeeded is unspecified (currently only on Chrome) - if (typeof el.scrollIntoViewIfNeeded === "function") { - // @ts-expect-error scrollIntoViewIfNeeded is unspecified - el.scrollIntoViewIfNeeded(); - } else { - el.scrollIntoView({ block: "nearest" }); + const el = listElm && listElm.querySelector(query); + if (el) { + // @ts-expect-error scrollIntoViewIfNeeded is unspecified (currently only on Chrome) + if (typeof el.scrollIntoViewIfNeeded === "function") { + // @ts-expect-error scrollIntoViewIfNeeded is unspecified + el.scrollIntoViewIfNeeded(); + } else { + el.scrollIntoView({ block: "nearest" }); + } } - } - }, 1); + }, 1); + } } function selectItem() { @@ -240,7 +258,9 @@ class:selected={i === highlightIndex} aria-selected={i === highlightIndex} tabindex="-1" - on:click={() => selectListItem(item, false)} + on:click|preventDefault={() => { + selectListItem(item, false); + }} on:keypress={(e) => { e.key == "Enter" && selectListItem(item, true); }} @@ -256,6 +276,11 @@ {/each}
    {/if} + + {#if idInputName} + + + {/if}