From 8630b6a32d850e93ff1b426a92f68400f7b2f666 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 5 May 2024 16:47:18 +0200 Subject: [PATCH 01/10] fix: light/dark theme --- src/app.pcss | 4 ++++ src/lib/components/filter/Autocomplete.svelte | 2 +- src/lib/components/ui/NavLink.svelte | 16 ++++++++-------- src/lib/components/ui/markdown/carta.pcss | 6 ++++++ src/lib/components/ui/markdown/carta.ts | 1 - src/routes/(app)/+layout.svelte | 11 ++++++----- tailwind.config.cjs | 12 ++++++++++++ 7 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/app.pcss b/src/app.pcss index 7ddf037..6a91435 100644 --- a/src/app.pcss +++ b/src/app.pcss @@ -15,6 +15,10 @@ } } +.badge { + border: none; +} + button { text-align: initial; } diff --git a/src/lib/components/filter/Autocomplete.svelte b/src/lib/components/filter/Autocomplete.svelte index 9a0de09..dcf26a7 100644 --- a/src/lib/components/filter/Autocomplete.svelte +++ b/src/lib/components/filter/Autocomplete.svelte @@ -348,7 +348,7 @@ top: 0px; max-height: calc(15 * (1rem + 10px) + 15px); user-select: none; - @apply bg-neutral text-neutral-content rounded-btn p-2; + @apply bg-base-100 text-base-content rounded-btn p-2 border border-base-content/30; } .autocomplete-list:empty { diff --git a/src/lib/components/ui/NavLink.svelte b/src/lib/components/ui/NavLink.svelte index 4a532d6..3dbc1dc 100644 --- a/src/lib/components/ui/NavLink.svelte +++ b/src/lib/components/ui/NavLink.svelte @@ -1,13 +1,13 @@ -
- -
+ diff --git a/src/lib/components/ui/markdown/carta.pcss b/src/lib/components/ui/markdown/carta.pcss index f325440..17e80dd 100644 --- a/src/lib/components/ui/markdown/carta.pcss +++ b/src/lib/components/ui/markdown/carta.pcss @@ -121,3 +121,9 @@ .carta-theme__default .carta-toolbar-left button.carta-active { @apply font-semibold border-primary; } + +@media (prefers-color-scheme: dark) { + .shiki, .shiki span { + color: var(--shiki-dark) !important; + } +} diff --git a/src/lib/components/ui/markdown/carta.ts b/src/lib/components/ui/markdown/carta.ts index 693f304..0d6b549 100644 --- a/src/lib/components/ui/markdown/carta.ts +++ b/src/lib/components/ui/markdown/carta.ts @@ -3,7 +3,6 @@ import type { Options } from "carta-md"; import { sanitizeHtml } from "$lib/shared/util"; export const CARTA_CFG: Options = { - theme: "github-dark", sanitizer: sanitizeHtml, disableIcons: ["taskList"], }; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 9385be4..28d8bb6 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -9,7 +9,7 @@
diff --git a/tailwind.config.cjs b/tailwind.config.cjs index db96d07..99f32dd 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,4 +1,7 @@ /** @type {import('tailwindcss').Config}*/ + +const themes = require("daisyui/src/theming/themes"); + const config = { content: [ "./src/**/*.{html,js,svelte,ts}", @@ -9,6 +12,15 @@ const config = { daisyui: { logs: false, + themes: [ + { + light: { + ...themes["light"], + "primary": "#799dff", + } + }, + "dark" + ], }, safelist: ["prose"], }; From 80c6243e2b1fe1c8c92b57809c094219be666065 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 5 May 2024 19:10:46 +0200 Subject: [PATCH 02/10] feat: add toast error messages --- package.json | 1 + pnpm-lock.yaml | 11 +++++++ src/app.pcss | 22 +++++++++++++ src/lib/components/filter/Autocomplete.svelte | 5 ++- src/lib/components/filter/SavedFilters.svelte | 32 +++++++++++++------ src/lib/components/form/CategoryForm.svelte | 2 ++ src/lib/components/form/PatientForm.svelte | 2 ++ src/lib/components/form/RoomForm.svelte | 2 ++ src/lib/components/form/StationForm.svelte | 2 ++ src/lib/shared/util/toast.ts | 8 +++++ src/lib/shared/util/util.ts | 22 +++++++++++++ src/routes/(app)/entry/[id]/+page.server.ts | 3 +- src/routes/(app)/entry/[id]/+page.svelte | 7 +++- src/routes/(app)/entry/[id]/edit/+page.svelte | 2 ++ .../entry/[id]/editExecution/+page.svelte | 3 ++ src/routes/(app)/entry/new/+page.svelte | 5 ++- src/routes/(app)/test/+page.svelte | 8 +++++ src/routes/+layout.svelte | 5 +++ 18 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 src/lib/shared/util/toast.ts create mode 100644 src/routes/(app)/test/+page.svelte diff --git a/package.json b/package.json index 9ad9517..19903e8 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@types/node": "^20.12.8", "@types/qs": "^6.9.15", "@types/set-cookie-parser": "^2.4.7", + "@zerodevx/svelte-toast": "^0.9.5", "autoprefixer": "^10.4.19", "daisyui": "^4.10.5", "dotenv": "^16.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1c54b7..8e84574 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ devDependencies: '@types/set-cookie-parser': specifier: ^2.4.7 version: 2.4.7 + '@zerodevx/svelte-toast': + specifier: ^0.9.5 + version: 0.9.5(svelte@4.2.15) autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.38) @@ -1448,6 +1451,14 @@ packages: pretty-format: 29.7.0 dev: true + /@zerodevx/svelte-toast@0.9.5(svelte@4.2.15): + resolution: {integrity: sha512-JLeB/oRdJfT+dz9A5bgd3Z7TuQnBQbeUtXrGIrNWMGqWbabpepBF2KxtWVhL2qtxpRqhae2f6NAOzH7xs4jUSw==} + peerDependencies: + svelte: ^3.57.0 || ^4.0.0 + dependencies: + svelte: 4.2.15 + dev: true + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: diff --git a/src/app.pcss b/src/app.pcss index 6a91435..244e176 100644 --- a/src/app.pcss +++ b/src/app.pcss @@ -15,6 +15,28 @@ } } +:root { + --toastBackground: oklch(var(--b3)); + --toastColor: oklch(var(--bc)); + --toastLeftBorder: oklch(var(--p)); + --toastBarBackground: oklch(var(--bc) / 0.4); + --toastContainerTop: 4rem; + --toastContainerRight: 1rem; + --toastBarHeight: 3px; +} + +.toast-error { + --toastLeftBorder: oklch(var(--er)); +} + +._toastItem { + border-left: solid 6px var(--toastLeftBorder) !important; +} + +._toastMsg { + white-space: pre-wrap; +} + .badge { border: none; } diff --git a/src/lib/components/filter/Autocomplete.svelte b/src/lib/components/filter/Autocomplete.svelte index dcf26a7..a2866ab 100644 --- a/src/lib/components/filter/Autocomplete.svelte +++ b/src/lib/components/filter/Autocomplete.svelte @@ -1,4 +1,5 @@ diff --git a/src/lib/components/form/CategoryForm.svelte b/src/lib/components/form/CategoryForm.svelte index 5b86ef0..00ccd63 100644 --- a/src/lib/components/form/CategoryForm.svelte +++ b/src/lib/components/form/CategoryForm.svelte @@ -7,6 +7,7 @@ import type { Category } from "$lib/shared/model"; import { ZCategoryNew } from "$lib/shared/model/validation"; + import { superformConfig } from "$lib/shared/util"; import FormField from "$lib/components/ui/FormField.svelte"; import Header from "$lib/components/ui/Header.svelte"; @@ -21,6 +22,7 @@ } = superForm(formData, { validators: schema, resetForm: category === null, + ...superformConfig("Kategorie"), }); diff --git a/src/lib/components/form/PatientForm.svelte b/src/lib/components/form/PatientForm.svelte index dfdec17..91a6ad5 100644 --- a/src/lib/components/form/PatientForm.svelte +++ b/src/lib/components/form/PatientForm.svelte @@ -7,6 +7,7 @@ import { ZPatientNew } from "$lib/shared/model/validation"; import { trpc } from "$lib/shared/trpc"; import type { RouterOutput } from "$lib/shared/trpc"; + import { superformConfig } from "$lib/shared/util"; import Autocomplete from "$lib/components/filter/Autocomplete.svelte"; import FormField from "$lib/components/ui/FormField.svelte"; @@ -23,6 +24,7 @@ } = superForm(formData, { validators: schema, resetForm: patient === null, + ...superformConfig("Patient"), }); diff --git a/src/lib/components/form/RoomForm.svelte b/src/lib/components/form/RoomForm.svelte index 0fb07ba..c906211 100644 --- a/src/lib/components/form/RoomForm.svelte +++ b/src/lib/components/form/RoomForm.svelte @@ -7,6 +7,7 @@ import { ZRoomNew } from "$lib/shared/model/validation"; import { trpc, type RouterOutput } from "$lib/shared/trpc"; + import { superformConfig } from "$lib/shared/util"; import Autocomplete from "$lib/components/filter/Autocomplete.svelte"; import FormField from "$lib/components/ui/FormField.svelte"; @@ -22,6 +23,7 @@ } = superForm(formData, { validators: schema, resetForm: room === null, + ...superformConfig("Zimmer"), }); diff --git a/src/lib/components/form/StationForm.svelte b/src/lib/components/form/StationForm.svelte index 93c1b93..d8ba548 100644 --- a/src/lib/components/form/StationForm.svelte +++ b/src/lib/components/form/StationForm.svelte @@ -7,6 +7,7 @@ import type { Station } from "$lib/shared/model"; import { ZStationNew } from "$lib/shared/model/validation"; + import { superformConfig } from "$lib/shared/util"; import FormField from "$lib/components/ui/FormField.svelte"; import Header from "$lib/components/ui/Header.svelte"; @@ -21,6 +22,7 @@ } = superForm(formData, { validators: schema, resetForm: station === null, + ...superformConfig("Station"), }); diff --git a/src/lib/shared/util/toast.ts b/src/lib/shared/util/toast.ts new file mode 100644 index 0000000..cf6962f --- /dev/null +++ b/src/lib/shared/util/toast.ts @@ -0,0 +1,8 @@ +import { toast } from "@zerodevx/svelte-toast"; + +export function toastInfo(msg: string) { + toast.push({ msg }); +} +export function toastError(msg: string) { + toast.push({ msg, classes: ["toast-error"] }); +} diff --git a/src/lib/shared/util/util.ts b/src/lib/shared/util/util.ts index d074d52..de97de0 100644 --- a/src/lib/shared/util/util.ts +++ b/src/lib/shared/util/util.ts @@ -4,11 +4,14 @@ import { isRedirect, error } from "@sveltejs/kit"; import { TRPCClientError } from "@trpc/client"; import DOMPurify from "isomorphic-dompurify"; import qs from "qs"; +import type { FormOptions } from "sveltekit-superforms"; import { ZodError } from "zod"; import type { EntityQuery } from "$lib/shared/model"; import type { RouterOutput } from "$lib/shared/trpc"; +import { toastError, toastInfo } from "./toast"; + export function formatBool(val: boolean): string { return val ? "Ja" : "Nein"; } @@ -107,3 +110,22 @@ export function divFloor(a: number, b: number): number { export function normalizeLineEndings(s: string): string { return s.replaceAll("\r\n", "\n"); } + +export function superformConfig(entity?: string): Pick { + return { + onError: ({ result }) => { + toastError(result.error.message); + }, + onResult: ({ result }) => { + if (result.type === "success") { + if (result.data?.form && typeof result.data.form.message === "string") { + toastInfo(result.data.form.message); + } else if (entity) { + toastInfo(entity + " aktualisiert"); + } else { + toastInfo("OK"); + } + } + }, + }; +} diff --git a/src/routes/(app)/entry/[id]/+page.server.ts b/src/routes/(app)/entry/[id]/+page.server.ts index 92565c6..693fb9c 100644 --- a/src/routes/(app)/entry/[id]/+page.server.ts +++ b/src/routes/(app)/entry/[id]/+page.server.ts @@ -1,7 +1,7 @@ import type { Actions } from "./$types"; import { fail } from "@sveltejs/kit"; -import { superValidate } from "sveltekit-superforms"; +import { superValidate, message } from "sveltekit-superforms"; import { ZUrlEntityId } from "$lib/shared/model/validation"; import { trpc } from "$lib/shared/trpc"; @@ -22,5 +22,6 @@ export const actions: Actions = { old_execution_id: null, execution: { text: form.data.text }, }); + return message(form, "Eintrag erledigt"); }), }; diff --git a/src/routes/(app)/entry/[id]/+page.svelte b/src/routes/(app)/entry/[id]/+page.svelte index 46a4e0f..7d0464d 100644 --- a/src/routes/(app)/entry/[id]/+page.svelte +++ b/src/routes/(app)/entry/[id]/+page.svelte @@ -4,6 +4,8 @@ import { defaults, superForm } from "sveltekit-superforms"; + import { superformConfig } from "$lib/shared/util"; + import EntryBody from "$lib/components/entry/EntryBody.svelte"; import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte"; @@ -12,8 +14,11 @@ export let data: PageData; const formData = defaults(SchemaEntryExecution); - const { form, errors, enhance } = superForm(formData, { + const { + form, errors, enhance, + } = superForm(formData, { validators: SchemaEntryExecution, + ...superformConfig("Eintrag"), }); diff --git a/src/routes/(app)/entry/[id]/edit/+page.svelte b/src/routes/(app)/entry/[id]/edit/+page.svelte index f29d052..8717329 100644 --- a/src/routes/(app)/entry/[id]/edit/+page.svelte +++ b/src/routes/(app)/entry/[id]/edit/+page.svelte @@ -6,6 +6,7 @@ import { trpc } from "$lib/shared/trpc"; import { formatDate, humanDate } from "$lib/shared/util"; + import { superformConfig } from "$lib/shared/util"; import PatientCard from "$lib/components/entry/PatientCard.svelte"; import Autocomplete from "$lib/components/filter/Autocomplete.svelte"; @@ -24,6 +25,7 @@ form, errors, constraints, enhance, tainted, } = superForm(data.form, { validators: SchemaNewEntryVersion, + ...superformConfig("Eintrag"), }); diff --git a/src/routes/(app)/entry/[id]/editExecution/+page.svelte b/src/routes/(app)/entry/[id]/editExecution/+page.svelte index b82c776..4e94ca4 100644 --- a/src/routes/(app)/entry/[id]/editExecution/+page.svelte +++ b/src/routes/(app)/entry/[id]/editExecution/+page.svelte @@ -4,6 +4,8 @@ import { defaults, superForm } from "sveltekit-superforms"; + import { superformConfig } from "$lib/shared/util"; + import EntryBody from "$lib/components/entry/EntryBody.svelte"; import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte"; @@ -14,6 +16,7 @@ const formData = defaults(SchemaNewExecution); const { form, errors, enhance } = superForm(formData, { validators: SchemaNewExecution, + ...superformConfig("Eintrag"), }); diff --git a/src/routes/(app)/entry/new/+page.svelte b/src/routes/(app)/entry/new/+page.svelte index 9ff091a..8030b63 100644 --- a/src/routes/(app)/entry/new/+page.svelte +++ b/src/routes/(app)/entry/new/+page.svelte @@ -2,6 +2,8 @@ import { defaults, superForm } from "sveltekit-superforms"; import { trpc } from "$lib/shared/trpc"; + import { superformConfig } from "$lib/shared/util"; + import { toastError } from "$lib/shared/util/toast"; import Autocomplete from "$lib/components/filter/Autocomplete.svelte"; import FormField from "$lib/components/ui/FormField.svelte"; @@ -14,6 +16,7 @@ form, errors, constraints, enhance, } = superForm(formData, { validators: SchemaNewEntryWithPatient, + ...superformConfig("Eintrag"), }); @@ -56,7 +59,7 @@ $form.patient_first_name = p.first_name; $form.patient_last_name = p.last_name; $form.patient_age = p.age; - }); + }).catch((e) => toastError("Konnte Patient nicht laden:\n" + e)); return { newValue: item.name ?? "", close: true }; }} onUnselect={() => { diff --git a/src/routes/(app)/test/+page.svelte b/src/routes/(app)/test/+page.svelte new file mode 100644 index 0000000..a08fe61 --- /dev/null +++ b/src/routes/(app)/test/+page.svelte @@ -0,0 +1,8 @@ + + +
+ + +
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 0d87d59..571d8e3 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,6 +3,8 @@ import { navigating } from "$app/stores"; + import { SvelteToast } from "@zerodevx/svelte-toast"; + import LoadingBar from "$lib/components/ui/LoadingBar.svelte"; import { screenWidth } from "$lib/stores/layout"; @@ -13,9 +15,12 @@ } else { loadingBar?.reset(); } + + const options = { pausable: true }; +
From 519ae01eeec294f7522c5ad9bcf8f2b6b44539fb Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 5 May 2024 19:36:39 +0200 Subject: [PATCH 03/10] fix: not using UTC dates for parsing date ranges in backend --- src/lib/server/query/entry.ts | 16 ++++++++++++++-- src/lib/shared/util/date.ts | 15 +++++---------- tests/integration/query/entry.ts | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/lib/server/query/entry.ts b/src/lib/server/query/entry.ts index 86ab620..5a52cc4 100644 --- a/src/lib/server/query/entry.ts +++ b/src/lib/server/query/entry.ts @@ -276,7 +276,7 @@ left join stations s on s.id = r.station_id`, if (filter?.date) { filterListToArray(filter.date).forEach((itm) => { - const dateRange = DateRange.parse(itm); + const dateRange = DateRange.parse(itm, true); if (dateRange?.start) { qb.addFilterClause(`ev.date >= ${qb.pvar()}`, dateRange.start); } @@ -405,7 +405,19 @@ export async function getNTodo(date: Date): Promise { order by ev2.created_at desc limit 1) - where ev.date <= ${date}`; + left join entry_executions ex on + ex.entry_id = e.id + and ex.id = ( + select + id + from + entry_executions ex2 + where + ex2.entry_id = ex.entry_id + order by + ex2.created_at desc + limit 1) + where ev.date <= ${date} and ex.id is null`; // @ts-expect-error type checked const count = Number(result[0].count); return count; diff --git a/src/lib/shared/util/date.ts b/src/lib/shared/util/date.ts index 43770bb..a9dcba4 100644 --- a/src/lib/shared/util/date.ts +++ b/src/lib/shared/util/date.ts @@ -109,9 +109,6 @@ export class DateRange { /** Create a date range of the current calendar week */ static thisWeek(): DateRange { const dayStart = new Date(); - // Correct for timezone - dayStart.setMinutes(dayStart.getMinutes() - dayStart.getTimezoneOffset()); - const todayWd = dayStart.getDay(); // Day starts at Sunday (0) const daysMinus = todayWd === 0 ? 6 : todayWd - 1; @@ -126,11 +123,11 @@ export class DateRange { * - Range with 2 ends: `2024-04-13..2024-04-20` * - Range with 1 end: `2024-04-13..`; `..2024-04-20` */ - static parse(s: string): DateRange | null { + static parse(s: string, utc = false): DateRange | null { const parts = s.split("..", 2); const parsed = parts.map((p) => { if (p.length === 0) return null; - return dateFromYMD(p); + return utc ? new Date(p) : dateFromYMD(p); }); if (parsed.length === 0 @@ -163,10 +160,8 @@ export class DateRange { /** Return a string representation for display purposes */ format(): string { - let res = ""; - if (this.start) res += formatDate(this.start); - res += " \u2013 "; - if (this.end) res += formatDate(this.end); - return res; + if (this.start === null) return "bis " + formatDate(this.end!); + if (this.end === null) return "ab " + formatDate(this.start); + return formatDate(this.start) + " \u2013 " + formatDate(this.end); } } diff --git a/tests/integration/query/entry.ts b/tests/integration/query/entry.ts index 3d8709e..b8c5e79 100644 --- a/tests/integration/query/entry.ts +++ b/tests/integration/query/entry.ts @@ -290,5 +290,5 @@ test("get entries", async () => { // NTodo const n = await getNTodo(new Date("2024-01-05")); - expect(n).toBe(2); + expect(n).toBe(1); }); From 21f145f5f08e4f44beecd3d373dfc69e5fc94f23 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 5 May 2024 19:50:05 +0200 Subject: [PATCH 04/10] feat: improve week selector, select all TODO items by default --- src/lib/components/filter/filters.ts | 16 ++++++++-------- src/lib/components/ui/WeekSelector.svelte | 20 ++++++++++++++++---- src/routes/(app)/visit/+page.ts | 2 +- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/lib/components/filter/filters.ts b/src/lib/components/filter/filters.ts index bd7ea43..70f2f16 100644 --- a/src/lib/components/filter/filters.ts +++ b/src/lib/components/filter/filters.ts @@ -16,22 +16,22 @@ import { import { WEEK_LIMIT } from "$lib/shared/constants"; import { trpc } from "$lib/shared/trpc"; -import { DateRange, dateToYMD } from "$lib/shared/util"; +import { DateRange } from "$lib/shared/util"; import { type FilterDef, InputType, type BaseItem } from "./types"; -export function weekFilterItems(earlierLater: boolean): BaseItem[] { +export function weekFilterItems(): BaseItem[] { const range = DateRange.thisWeek(); - const res = []; - if (earlierLater) res.push({ id: ".." + dateToYMD(range.start!), name: "Früher" }); + const res: BaseItem[] = []; + const addRange = (r: DateRange) => res.push({ id: r.toString(), name: r.format() }); + addRange(new DateRange(null, range.start)); for (let i = 0; i < WEEK_LIMIT; i++) { - res.push({ id: range.toString(), name: range.format() }); + addRange(range); range.addDays(7); } - - if (earlierLater) res.push({ id: dateToYMD(range.start!) + "..", name: "Später" }); + addRange(new DateRange(range.start, null)); return res; } @@ -105,7 +105,7 @@ export const ENTRY_FILTERS: { [key: string]: FilterDef } = { name: "Woche", icon: mdiCalendar, inputType: InputType.FilterList, - options: async () => weekFilterItems(true), + options: async () => weekFilterItems(), }, }; diff --git a/src/lib/components/ui/WeekSelector.svelte b/src/lib/components/ui/WeekSelector.svelte index 3242800..de0e54c 100644 --- a/src/lib/components/ui/WeekSelector.svelte +++ b/src/lib/components/ui/WeekSelector.svelte @@ -12,17 +12,29 @@ let editing = false; let autocomplete: Autocomplete | undefined; - export let dateRange: DateRange = DateRange.thisWeek(); + export let dateRange: DateRange = new DateRange(null, DateRange.thisWeek().end); export let onSelect: (value: DateRange) => void = () => {}; + function addDays(n: number) { + if (dateRange.start === null) { + dateRange.start = new Date(dateRange.end!); + dateRange.start.setDate(dateRange.start.getDate() - 6); + } else if (dateRange.end === null) { + dateRange.end = new Date(dateRange.start!); + dateRange.end.setDate(dateRange.end.getDate() + 6); + } else { + dateRange.addDays(n); + } + } + function nextWeek() { - dateRange.addDays(7); + addDays(7); dateRange = dateRange; // update reactive onSelect(dateRange); } function previousWeek() { - dateRange.addDays(-7); + addDays(-7); dateRange = dateRange; // update reactive onSelect(dateRange); } @@ -45,7 +57,7 @@ {#if editing} weekFilterItems(false)} + items={async () => weekFilterItems()} onClose={stopEditing} onSelect={(item) => { if (typeof item.id === "string") { diff --git a/src/routes/(app)/visit/+page.ts b/src/routes/(app)/visit/+page.ts index bd1917b..ef115ba 100644 --- a/src/routes/(app)/visit/+page.ts +++ b/src/routes/(app)/visit/+page.ts @@ -23,7 +23,7 @@ export const load: PageLoad = async (event) => { const url = getQueryUrl({ filter: { done: false, - date: [{ id: DateRange.thisWeek().toString() }], + date: [{ id: new DateRange(null, DateRange.thisWeek().end).toString() }], }, }, URL_VISIT); redirect(302, url); From f36ae71d32ebcfc5054dca9c6907ffc1554f39e3 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 6 May 2024 15:20:41 +0200 Subject: [PATCH 05/10] feat: use global store for saved filters, add default filters --- src/lib/components/filter/Autocomplete.svelte | 4 +- src/lib/components/filter/FilterBar.svelte | 4 +- src/lib/components/filter/FilterChip.svelte | 2 +- src/lib/components/filter/SavedFilters.svelte | 63 +++++---- src/lib/components/filter/filters.ts | 4 +- src/lib/components/filter/types.ts | 7 +- .../components/ui/PaginationButtons.svelte | 2 +- src/lib/server/query/entry.ts | 2 +- src/lib/server/query/patient.ts | 2 +- src/lib/server/query/savedFilter.ts | 40 +++++- src/lib/server/trpc/routes/savedFilter.ts | 13 +- src/lib/shared/constants.ts | 2 + src/lib/shared/util/util.ts | 29 +++- src/lib/stores/index.ts | 9 ++ src/lib/stores/layout.ts | 5 - src/routes/(app)/+layout.svelte | 14 +- src/routes/(app)/+layout.ts | 9 ++ src/routes/(app)/+page.svelte | 6 +- src/routes/(app)/visit/+page.ts | 13 +- src/routes/+layout.svelte | 2 +- tests/helpers/reset-db.ts | 1 + tests/integration/query/savedFilter.ts | 129 ++++++++++++++++++ 22 files changed, 294 insertions(+), 68 deletions(-) create mode 100644 src/lib/stores/index.ts delete mode 100644 src/lib/stores/layout.ts create mode 100644 src/routes/(app)/+layout.ts create mode 100644 tests/integration/query/savedFilter.ts diff --git a/src/lib/components/filter/Autocomplete.svelte b/src/lib/components/filter/Autocomplete.svelte index a2866ab..304ed85 100644 --- a/src/lib/components/filter/Autocomplete.svelte +++ b/src/lib/components/filter/Autocomplete.svelte @@ -32,7 +32,7 @@ /** Set of item IDs that should be hidden from the list */ export let hiddenIds: Set = new Set(); /** Object to cache fetched items in */ - export let cache: { [key: string]: T[] } = {}; + export let cache: Record = {}; /** Key in cache object under which fetched items are stored */ export let cacheKey: string | undefined = undefined; /** Input field placeholder */ @@ -188,7 +188,7 @@ function onKeyDown(e: KeyboardEvent) { let { key } = e; if (key === "Tab" && e.shiftKey) key = "ShiftTab"; - const fnmap: { [key: string]: () => void } = { + const fnmap: Record void> = { Tab: () => close, ShiftTab: () => close, ArrowDown: () => { diff --git a/src/lib/components/filter/FilterBar.svelte b/src/lib/components/filter/FilterBar.svelte index 2926571..6ad4925 100644 --- a/src/lib/components/filter/FilterBar.svelte +++ b/src/lib/components/filter/FilterBar.svelte @@ -19,7 +19,7 @@ import { InputType, isFilterValueless } from "./types"; /** Filter definitions */ - export let FILTERS: { [key: string]: FilterDef }; + export let FILTERS: Record; /** Filter data from the query */ export let filterData: FilterQdata | null | undefined; /** Callback when filters are updated */ @@ -32,7 +32,7 @@ let autocomplete: Autocomplete | undefined; let activeFilters: FilterData[] = []; - const cache: { [key: string]: BaseItem[] } = {}; + const cache: Record = {}; let searchVal = ""; const searchDebounce = new Debouncer(400, () => { onUpdate(getFilterQdata()); diff --git a/src/lib/components/filter/FilterChip.svelte b/src/lib/components/filter/FilterChip.svelte index dcc38f0..2f8d7ec 100644 --- a/src/lib/components/filter/FilterChip.svelte +++ b/src/lib/components/filter/FilterChip.svelte @@ -11,7 +11,7 @@ export let filter: FilterDef; export let hiddenIds: () => Set = () => new Set(); - export let cache: { [key: string]: BaseItem[] } = {}; + export let cache: Record = {}; export let fdata: FilterData; export let onRemove = () => {}; diff --git a/src/lib/components/filter/SavedFilters.svelte b/src/lib/components/filter/SavedFilters.svelte index 2f976d2..206f957 100644 --- a/src/lib/components/filter/SavedFilters.svelte +++ b/src/lib/components/filter/SavedFilters.svelte @@ -1,31 +1,39 @@ @@ -73,19 +82,15 @@ Gespeicherte Filter:
- {#if filters} - {#each filters as filter, i (filter.id)} - remove(i)} - onSave={() => update(i)} - > - {filter.name} - - {/each} - {:else} - - {/if} + {#each filters as filter, i (filter.id)} + remove(i)} + onSave={() => update(i)} + > + {filter.name} + + {/each} +{:else if hasEntries} + +{:else} + +{/if} diff --git a/src/lib/components/table/PatientTable.svelte b/src/lib/components/table/PatientTable.svelte index 2fcd3f3..868df6c 100644 --- a/src/lib/components/table/PatientTable.svelte +++ b/src/lib/components/table/PatientTable.svelte @@ -63,9 +63,6 @@ > - {/each} diff --git a/src/lib/components/ui/HiddenToggle.svelte b/src/lib/components/ui/HiddenToggle.svelte new file mode 100644 index 0000000..abe74b4 --- /dev/null +++ b/src/lib/components/ui/HiddenToggle.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/lib/server/query/category.ts b/src/lib/server/query/category.ts index 0f5f9aa..05226b6 100644 --- a/src/lib/server/query/category.ts +++ b/src/lib/server/query/category.ts @@ -1,7 +1,13 @@ -import type { Category, CategoryNew } from "$lib/shared/model"; +import type { Category, CategoryDetail, CategoryNew } from "$lib/shared/model"; import { prisma } from "$lib/server/prisma"; +import { handleDeleteConflict } from "./util"; + +const SELECT = { + id: true, name: true, description: true, color: true, +}; + export async function newCategory(category: CategoryNew): Promise { const created = await prisma.category.create({ data: category, @@ -10,18 +16,26 @@ export async function newCategory(category: CategoryNew): Promise { return created.id; } -export async function updateCategory(id: number, category: Partial) { +export async function updateCategory(id: number, category: Partial): Promise { await prisma.category.update({ where: { id }, data: category }); } -export async function deleteCategory(id: number) { - await prisma.category.delete({ where: { id } }); +export async function deleteCategory(id: number): Promise { + await handleDeleteConflict(prisma.category.delete({ where: { id } }), "category with entries"); } -export async function getCategory(id: number): Promise { +export async function hideCategory(id: number, hidden: boolean): Promise { + await prisma.category.update({ where: { id }, data: { hidden } }); +} + +export async function getCategory(id: number): Promise { return prisma.category.findUniqueOrThrow({ where: { id } }); } -export async function getCategories(): Promise { - return prisma.category.findMany({ orderBy: { id: "asc" } }); +export async function getCategories(hidden = false): Promise { + return prisma.category.findMany({ select: SELECT, where: { hidden }, orderBy: { id: "asc" } }); +} + +export async function getCategoryNEntries(id: number): Promise { + return prisma.entry.count({ where: { EntryVersion: { some: { category_id: id } } } }); } diff --git a/src/lib/server/query/index.ts b/src/lib/server/query/index.ts index 220256b..c35bfb2 100644 --- a/src/lib/server/query/index.ts +++ b/src/lib/server/query/index.ts @@ -3,3 +3,4 @@ export * from "./category"; export * from "./patient"; export * from "./user"; export * from "./room"; +export * from "./station"; diff --git a/src/lib/server/query/mapping.ts b/src/lib/server/query/mapping.ts index 1be7996..ad7e041 100644 --- a/src/lib/server/query/mapping.ts +++ b/src/lib/server/query/mapping.ts @@ -14,7 +14,6 @@ import type { Patient, User, UserTag, - Room, EntryVersion, EntryExecution, UserTagNameNonnull, @@ -65,10 +64,6 @@ export function mapUserTagNameNonnull(user: Omit): UserTagNameN return { id: user.id, name: user.name || "" }; } -export function mapRoom(room: DbRoomLn): Room { - return { id: room.id, name: room.name, station: room.station }; -} - export function mapVersion(version: DbEntryVersionLn): EntryVersion { return { id: version.id, diff --git a/src/lib/server/query/patient.ts b/src/lib/server/query/patient.ts index bccf37f..5180280 100644 --- a/src/lib/server/query/patient.ts +++ b/src/lib/server/query/patient.ts @@ -1,5 +1,3 @@ -import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; - import type { Patient, PatientNew, @@ -9,12 +7,12 @@ import type { PatientTag, SortRequest, } from "$lib/shared/model"; -import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error"; +import { ErrorInvalidInput } from "$lib/shared/util/error"; import { prisma } from "$lib/server/prisma"; import { mapPatient } from "./mapping"; -import { QueryBuilder } from "./util"; +import { QueryBuilder, handleDeleteConflict } from "./util"; export async function newPatient(patient: PatientNew): Promise { const created = await prisma.patient.create({ data: patient, select: { id: true } }); @@ -22,27 +20,17 @@ export async function newPatient(patient: PatientNew): Promise { } /** Update a patient */ -export async function updatePatient(id: number, patient: Partial) { +export async function updatePatient(id: number, patient: Partial): Promise { await prisma.patient.update({ where: { id }, data: patient }); } /** Delete a patient (Note: this only works if the patient is not associated with any entries) */ -export async function deletePatient(id: number) { - try { - await prisma.patient.delete({ where: { id } }); - } catch (error) { - if (error instanceof PrismaClientKnownRequestError) { - // Foreign key constraint failed - if (error.code === "P2003") { - throw new ErrorConflict("cannot delete patient with entries"); - } - } - throw error; - } +export async function deletePatient(id: number): Promise { + await handleDeleteConflict(prisma.patient.delete({ where: { id } }), "patient with entries"); } /** Hide/show a patient */ -export async function hidePatient(id: number, hidden: boolean) { +export async function hidePatient(id: number, hidden: boolean): Promise { await prisma.patient.update({ where: { id }, data: { hidden } }); } diff --git a/src/lib/server/query/room.ts b/src/lib/server/query/room.ts index 1e6b9a5..eecf463 100644 --- a/src/lib/server/query/room.ts +++ b/src/lib/server/query/room.ts @@ -1,61 +1,43 @@ import type { - RoomNew, Room, Station, StationNew, + RoomNew, Room, + RoomDetail, } from "$lib/shared/model"; import { prisma } from "$lib/server/prisma"; -import { mapRoom } from "./mapping"; +import { handleDeleteConflict } from "./util"; -export async function newStation(station: StationNew): Promise { - const created = await prisma.station.create({ data: station, select: { id: true } }); - return created.id; -} - -export async function updateStation(id: number, station: Partial) { - await prisma.station.update({ where: { id }, data: station }); -} - -export async function deleteStation(id: number) { - await prisma.station.delete({ where: { id } }); -} - -export async function getStation(id: number): Promise { - return prisma.station.findUniqueOrThrow({ where: { id } }); -} - -export async function getStations(): Promise { - return prisma.station.findMany({ orderBy: { id: "asc" } }); -} +const SELECT = { id: true, name: true, station: { select: { id: true, name: true } } }; export async function newRoom(room: RoomNew): Promise { const created = await prisma.room.create({ data: room, select: { id: true } }); return created.id; } -export async function updateRoom(id: number, room: Partial) { +export async function updateRoom(id: number, room: Partial): Promise { await prisma.room.update({ where: { id }, data: room }); } -export async function deleteRoom(id: number) { - await prisma.room.delete({ where: { id } }); +export async function deleteRoom(id: number): Promise { + await handleDeleteConflict(prisma.room.delete({ where: { id } }), "room with patients"); } -export async function getRoom(id: number): Promise { - const room = await prisma.room.findUniqueOrThrow({ - where: { id }, - include: { station: true }, - }); - return { - id: room.id, - name: room.name, - station: room.station, - }; +export async function hideRoom(id: number, hidden: boolean): Promise { + await prisma.room.update({ where: { id }, data: { hidden } }); } -export async function getRooms(): Promise { - const rooms = await prisma.room.findMany({ - include: { station: true }, +export async function getRoom(id: number): Promise { + return prisma.room.findUniqueOrThrow({ select: { ...SELECT, hidden: true }, where: { id } }); +} + +export async function getRooms(hidden = false): Promise { + return prisma.room.findMany({ + select: SELECT, + where: { hidden }, orderBy: [{ station: { name: "asc" } }, { name: "asc" }], }); - return rooms.map(mapRoom); +} + +export async function getRoomNPatients(id: number): Promise { + return prisma.patient.count({ where: { room_id: id } }); } diff --git a/src/lib/server/query/station.ts b/src/lib/server/query/station.ts new file mode 100644 index 0000000..dfdeedd --- /dev/null +++ b/src/lib/server/query/station.ts @@ -0,0 +1,40 @@ +import type { Station, StationDetail, StationNew } from "$lib/shared/model"; + +import { prisma } from "$lib/server/prisma"; + +import { handleDeleteConflict } from "./util"; + +const SELECT = { id: true, name: true }; + +export async function newStation(station: StationNew): Promise { + const created = await prisma.station.create({ data: station, select: { id: true } }); + return created.id; +} + +export async function updateStation(id: number, station: Partial) { + await prisma.station.update({ where: { id }, data: station }); +} + +export async function deleteStation(id: number) { + await handleDeleteConflict(prisma.station.delete({ where: { id } }), "station with rooms"); +} + +export async function hideStation(id: number, hidden: boolean) { + await prisma.station.update({ where: { id }, data: { hidden } }); +} + +export async function getStation(id: number): Promise { + return prisma.station.findUniqueOrThrow({ where: { id } }); +} + +export async function getStations(hidden = false): Promise { + return prisma.station.findMany({ + select: SELECT, + where: { hidden }, + orderBy: { id: "asc" }, + }); +} + +export async function getStationNRooms(id: number): Promise { + return prisma.room.count({ where: { station_id: id } }); +} diff --git a/src/lib/server/query/util.ts b/src/lib/server/query/util.ts index 9fd629f..8c57534 100644 --- a/src/lib/server/query/util.ts +++ b/src/lib/server/query/util.ts @@ -1,5 +1,8 @@ +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; + import { PAGINATION_LIMIT } from "$lib/shared/constants"; import type { FilterList, PaginationRequest } from "$lib/shared/model"; +import { ErrorConflict } from "$lib/shared/util/error"; enum QueryComponentType { Normal = 1, @@ -20,6 +23,20 @@ export function filterListToArray(fl: FilterList): T[] { return [fl]; } +export async function handleDeleteConflict(act: Promise, msg: string) { + try { + await act; + } catch (error) { + if (error instanceof PrismaClientKnownRequestError) { + // Foreign key constraint failed + if (error.code === "P2003") { + throw new ErrorConflict(`cannot delete ${msg}; hide instead`); + } + } + throw error; + } +} + class SearchQueryComponent { word: string; diff --git a/src/lib/server/trpc/routes/category.ts b/src/lib/server/trpc/routes/category.ts index 2333808..b5f3aa3 100644 --- a/src/lib/server/trpc/routes/category.ts +++ b/src/lib/server/trpc/routes/category.ts @@ -1,11 +1,15 @@ import { z } from "zod"; -import { ZEntityId, ZCategoryNew } from "$lib/shared/model/validation"; +import { + ZEntityId, ZCategoryNew, ZHide, ZListHidden, +} from "$lib/shared/model/validation"; import { deleteCategory, getCategories, getCategory, + getCategoryNEntries, + hideCategory, newCategory, updateCategory, } from "$lib/server/query"; @@ -13,10 +17,18 @@ import { import { t, trpcWrap } from ".."; export const categoryRouter = t.router({ - list: t.procedure.query(async () => trpcWrap(getCategories)), + list: t.procedure.input(ZListHidden).query(async (opts) => trpcWrap( + async () => getCategories(opts.input?.hidden), + )), get: t.procedure .input(ZEntityId) - .query(async (opts) => trpcWrap(async () => getCategory(opts.input))), + .query(async (opts) => trpcWrap(async () => { + const [category, n_entries] = await Promise.all([ + getCategory(opts.input), + getCategoryNEntries(opts.input), + ]); + return { ...category, n_entries }; + })), create: t.procedure.input(ZCategoryNew).mutation(async (opts) => trpcWrap(async () => { const id = await newCategory(opts.input); return id; @@ -29,4 +41,7 @@ export const categoryRouter = t.router({ delete: t.procedure.input(ZEntityId).mutation(async (opts) => trpcWrap(async () => { await deleteCategory(opts.input); })), + hide: t.procedure.input(ZHide).mutation(async (opts) => trpcWrap(async () => { + await hideCategory(opts.input.id, opts.input.hidden); + })), }); diff --git a/src/lib/server/trpc/routes/patient.ts b/src/lib/server/trpc/routes/patient.ts index dfe77a3..2f90645 100644 --- a/src/lib/server/trpc/routes/patient.ts +++ b/src/lib/server/trpc/routes/patient.ts @@ -1,6 +1,8 @@ import { z } from "zod"; -import { ZEntityId, ZPatientNew, ZPatientsQuery } from "$lib/shared/model/validation"; +import { + ZEntityId, ZHide, ZPatientNew, ZPatientsQuery, +} from "$lib/shared/model/validation"; import { deletePatient, @@ -42,14 +44,7 @@ export const patientRouter = t.router({ delete: t.procedure.input(ZEntityId).mutation(async (opts) => trpcWrap(async () => { await deletePatient(opts.input); })), - hide: t.procedure - .input( - z.object({ - id: ZEntityId, - hidden: z.boolean().default(true), - }), - ) - .mutation(async (opts) => trpcWrap(async () => { - await hidePatient(opts.input.id, opts.input.hidden); - })), + hide: t.procedure.input(ZHide).mutation(async (opts) => trpcWrap(async () => { + await hidePatient(opts.input.id, opts.input.hidden); + })), }); diff --git a/src/lib/server/trpc/routes/room.ts b/src/lib/server/trpc/routes/room.ts index 9872f2d..c0e2bc9 100644 --- a/src/lib/server/trpc/routes/room.ts +++ b/src/lib/server/trpc/routes/room.ts @@ -1,18 +1,28 @@ import { z } from "zod"; -import { ZEntityId, ZRoomNew } from "$lib/shared/model/validation"; +import { + ZEntityId, ZHide, ZListHidden, ZRoomNew, +} from "$lib/shared/model/validation"; import { - deleteRoom, getRoom, getRooms, newRoom, updateRoom, + deleteRoom, getRoom, getRoomNPatients, getRooms, hideRoom, newRoom, updateRoom, } from "$lib/server/query"; import { t, trpcWrap } from ".."; export const roomRouter = t.router({ - list: t.procedure.query(async (opts) => trpcWrap(getRooms)), + list: t.procedure.input(ZListHidden).query(async (opts) => trpcWrap( + async () => getRooms(opts.input?.hidden), + )), get: t.procedure .input(ZEntityId) - .query(async (opts) => trpcWrap(async () => getRoom(opts.input))), + .query(async (opts) => trpcWrap(async () => { + const [room, n_patients] = await Promise.all([ + getRoom(opts.input), + getRoomNPatients(opts.input), + ]); + return { ...room, n_patients }; + })), create: t.procedure.input(ZRoomNew).mutation(async (opts) => trpcWrap(async () => { const id = await newRoom(opts.input); return id; @@ -25,4 +35,7 @@ export const roomRouter = t.router({ delete: t.procedure.input(ZEntityId).mutation(async (opts) => trpcWrap(async () => { await deleteRoom(opts.input); })), + hide: t.procedure.input(ZHide).mutation(async (opts) => trpcWrap(async () => { + await hideRoom(opts.input.id, opts.input.hidden); + })), }); diff --git a/src/lib/server/trpc/routes/station.ts b/src/lib/server/trpc/routes/station.ts index d611f86..d9d68aa 100644 --- a/src/lib/server/trpc/routes/station.ts +++ b/src/lib/server/trpc/routes/station.ts @@ -1,11 +1,15 @@ import { z } from "zod"; -import { ZEntityId, ZStationNew } from "$lib/shared/model/validation"; +import { + ZEntityId, ZHide, ZListHidden, ZStationNew, +} from "$lib/shared/model/validation"; import { deleteStation, getStation, + getStationNRooms, getStations, + hideStation, newStation, updateStation, } from "$lib/server/query"; @@ -13,10 +17,16 @@ import { import { t, trpcWrap } from ".."; export const stationRouter = t.router({ - list: t.procedure.query(getStations), + list: t.procedure.input(ZListHidden).query(async (opts) => getStations(opts.input?.hidden)), get: t.procedure .input(ZEntityId) - .query(async (opts) => trpcWrap(async () => getStation(opts.input))), + .query(async (opts) => trpcWrap(async () => { + const [station, n_rooms] = await Promise.all([ + getStation(opts.input), + getStationNRooms(opts.input), + ]); + return { ...station, n_rooms }; + })), create: t.procedure.input(ZStationNew).mutation(async (opts) => trpcWrap(async () => { const id = await newStation(opts.input); return id; @@ -29,4 +39,7 @@ export const stationRouter = t.router({ delete: t.procedure.input(ZEntityId).mutation(async (opts) => trpcWrap(async () => { await deleteStation(opts.input); })), + hide: t.procedure.input(ZHide).mutation(async (opts) => trpcWrap(async () => { + await hideStation(opts.input.id, opts.input.hidden); + })), }); diff --git a/src/lib/shared/model/model.ts b/src/lib/shared/model/model.ts index e3c75de..bbecac8 100644 --- a/src/lib/shared/model/model.ts +++ b/src/lib/shared/model/model.ts @@ -35,31 +35,40 @@ export type UserTagNameNonnull = { name: string; }; -export type Station = { +export type StationDetail = { id: number; name: string; + hidden: boolean; }; +export type Station = Omit; + export type StationNew = Omit; -export type Room = { +export type RoomDetail = { id: number; name: string; station: Station; + hidden: boolean; }; +export type Room = Omit; + export type RoomNew = { name: string; station_id: number; }; -export type Category = { +export type CategoryDetail = { id: number; name: string; color: Option; description: Option; + hidden: boolean; }; +export type Category = Omit; + export type CategoryNew = Omit; export type Patient = { diff --git a/src/lib/shared/model/validation.ts b/src/lib/shared/model/validation.ts index 8c78b06..e422f28 100644 --- a/src/lib/shared/model/validation.ts +++ b/src/lib/shared/model/validation.ts @@ -181,3 +181,10 @@ export const ZSavedFilterUpdate = z.object({ id: ZEntityId, query: z.string(), }); + +export const ZHide = z.object({ + id: ZEntityId, + hidden: z.boolean().default(true), +}); + +export const ZListHidden = z.object({ hidden: z.boolean().optional() }).optional(); diff --git a/src/routes/(app)/categories/+page.svelte b/src/routes/(app)/categories/+page.svelte index bf183e0..9ae3771 100644 --- a/src/routes/(app)/categories/+page.svelte +++ b/src/routes/(app)/categories/+page.svelte @@ -3,6 +3,7 @@ import CategoryField from "$lib/components/table/CategoryField.svelte"; import Header from "$lib/components/ui/Header.svelte"; + import HiddenToggle from "$lib/components/ui/HiddenToggle.svelte"; export let data: PageData; @@ -11,8 +12,11 @@ Kategorien -
- Neue Kategorie +
+
diff --git a/src/routes/(app)/categories/+page.ts b/src/routes/(app)/categories/+page.ts index 6845c60..c3b130f 100644 --- a/src/routes/(app)/categories/+page.ts +++ b/src/routes/(app)/categories/+page.ts @@ -5,7 +5,8 @@ import { loadWrap } from "$lib/shared/util"; export const load: PageLoad = async (event) => { return loadWrap(async () => { - const categories = await trpc(event).category.list.query(); - return { categories }; + const hidden = event.url.searchParams.get("hidden") !== null; + const categories = await trpc(event).category.list.query({ hidden }); + return { categories, hidden }; }); }; diff --git a/src/routes/(app)/category/[id]/+page.server.ts b/src/routes/(app)/category/[id]/+page.server.ts index 07968b3..9851d12 100644 --- a/src/routes/(app)/category/[id]/+page.server.ts +++ b/src/routes/(app)/category/[id]/+page.server.ts @@ -1,7 +1,7 @@ import type { Actions } from "./$types"; import { fail, redirect } from "@sveltejs/kit"; -import { superValidate } from "sveltekit-superforms"; +import { message, superValidate } from "sveltekit-superforms"; import { zod } from "sveltekit-superforms/adapters"; import { ZCategoryNew, ZUrlEntityId } from "$lib/shared/model/validation"; @@ -12,14 +12,18 @@ export const actions: Actions = { default: async (event) => loadWrap(async () => { const id = ZUrlEntityId.parse(event.params.id); const formData = await event.request.formData(); + const form = await superValidate(formData, zod(ZCategoryNew)); + const hide = formData.get("hide"); const del = formData.get("delete"); - if (del) { + if (hide) { + const hidden = Boolean(parseInt(hide.toString())); + await trpc(event).category.hide.mutate({ id, hidden }); + return message(form, "Kategorie " + (hidden ? "ausgeblendet" : "eingeblendet")); + } else if (del) { await trpc(event).category.delete.mutate(id); redirect(302, "/categories"); } else { - const form = await superValidate(formData, zod(ZCategoryNew)); - if (!form.valid) { return fail(400, { form }); } @@ -28,8 +32,7 @@ export const actions: Actions = { id, category: form.data, }); - - return { form }; } + return { form }; }), }; diff --git a/src/routes/(app)/category/[id]/+page.svelte b/src/routes/(app)/category/[id]/+page.svelte index a311900..f27f3b7 100644 --- a/src/routes/(app)/category/[id]/+page.svelte +++ b/src/routes/(app)/category/[id]/+page.svelte @@ -2,6 +2,7 @@ import type { PageData } from "./$types"; import CategoryForm from "$lib/components/form/CategoryForm.svelte"; + import HideDelete from "$lib/components/form/HideDelete.svelte"; export let data: PageData; @@ -11,5 +12,5 @@ - + 0} hidden={data.category.hidden} /> diff --git a/src/routes/(app)/patient/[id]/+page.server.ts b/src/routes/(app)/patient/[id]/+page.server.ts index 55e08a2..e32dc20 100644 --- a/src/routes/(app)/patient/[id]/+page.server.ts +++ b/src/routes/(app)/patient/[id]/+page.server.ts @@ -1,7 +1,7 @@ import type { Actions } from "./$types"; import { fail, redirect } from "@sveltejs/kit"; -import { superValidate } from "sveltekit-superforms"; +import { message, superValidate } from "sveltekit-superforms"; import { zod } from "sveltekit-superforms/adapters"; import { ZPatientNew, ZUrlEntityId } from "$lib/shared/model/validation"; @@ -12,20 +12,18 @@ export const actions: Actions = { default: async (event) => loadWrap(async () => { const id = ZUrlEntityId.parse(event.params.id); const formData = await event.request.formData(); + const form = await superValidate(formData, zod(ZPatientNew)); const hide = formData.get("hide"); const del = formData.get("delete"); if (hide) { - await trpc(event).patient.hide.mutate({ - id, - hidden: Boolean(parseInt(hide.toString())), - }); + const hidden = Boolean(parseInt(hide.toString())); + await trpc(event).patient.hide.mutate({ id, hidden }); + return message(form, "Patient " + (hidden ? "ausgeblendet" : "eingeblendet")); } else if (del) { await trpc(event).patient.delete.mutate(id); redirect(302, "/patients"); } else { - const form = await superValidate(formData, zod(ZPatientNew)); - if (!form.valid) { return fail(400, { form }); } @@ -34,8 +32,7 @@ export const actions: Actions = { id, patient: form.data, }); - - return { form }; } + return { form }; }), }; diff --git a/src/routes/(app)/patient/[id]/+page.svelte b/src/routes/(app)/patient/[id]/+page.svelte index ad88658..6fd3d82 100644 --- a/src/routes/(app)/patient/[id]/+page.svelte +++ b/src/routes/(app)/patient/[id]/+page.svelte @@ -1,6 +1,7 @@ @@ -10,8 +11,11 @@ Zimmer -
- Neues Zimmer +
+
+
diff --git a/src/routes/(app)/rooms/+page.ts b/src/routes/(app)/rooms/+page.ts index 774e5e6..87a0e36 100644 --- a/src/routes/(app)/rooms/+page.ts +++ b/src/routes/(app)/rooms/+page.ts @@ -5,7 +5,8 @@ import { loadWrap } from "$lib/shared/util"; export const load: PageLoad = async (event) => { return loadWrap(async () => { - const rooms = await trpc(event).room.list.query(); - return { rooms }; + const hidden = event.url.searchParams.get("hidden") !== null; + const rooms = await trpc(event).room.list.query({ hidden }); + return { rooms, hidden }; }); }; diff --git a/src/routes/(app)/station/[id]/+page.server.ts b/src/routes/(app)/station/[id]/+page.server.ts index d03c112..ee30f40 100644 --- a/src/routes/(app)/station/[id]/+page.server.ts +++ b/src/routes/(app)/station/[id]/+page.server.ts @@ -1,7 +1,7 @@ import type { Actions } from "./$types"; import { fail, redirect } from "@sveltejs/kit"; -import { superValidate } from "sveltekit-superforms"; +import { message, superValidate } from "sveltekit-superforms"; import { zod } from "sveltekit-superforms/adapters"; import { ZStationNew, ZUrlEntityId } from "$lib/shared/model/validation"; @@ -12,14 +12,18 @@ export const actions: Actions = { default: async (event) => loadWrap(async () => { const id = ZUrlEntityId.parse(event.params.id); const formData = await event.request.formData(); + const form = await superValidate(formData, zod(ZStationNew)); + const hide = formData.get("hide"); const del = formData.get("delete"); - if (del) { + if (hide) { + const hidden = Boolean(parseInt(hide.toString())); + await trpc(event).station.hide.mutate({ id, hidden }); + return message(form, "Station " + (hidden ? "ausgeblendet" : "eingeblendet")); + } else if (del) { await trpc(event).station.delete.mutate(id); redirect(302, "/stations"); } else { - const form = await superValidate(formData, zod(ZStationNew)); - if (!form.valid) { return fail(400, { form }); } @@ -28,8 +32,7 @@ export const actions: Actions = { id, station: form.data, }); - - return { form }; } + return { form }; }), }; diff --git a/src/routes/(app)/station/[id]/+page.svelte b/src/routes/(app)/station/[id]/+page.svelte index 34531e1..2f99db3 100644 --- a/src/routes/(app)/station/[id]/+page.svelte +++ b/src/routes/(app)/station/[id]/+page.svelte @@ -1,6 +1,7 @@ @@ -10,8 +11,11 @@ Stationen -
- Neue Station +
+
+
diff --git a/src/routes/(app)/stations/+page.ts b/src/routes/(app)/stations/+page.ts index 679ca82..747cc98 100644 --- a/src/routes/(app)/stations/+page.ts +++ b/src/routes/(app)/stations/+page.ts @@ -5,7 +5,8 @@ import { loadWrap } from "$lib/shared/util"; export const load: PageLoad = async (event) => { return loadWrap(async () => { - const stations = await trpc(event).station.list.query(); - return { stations }; + const hidden = event.url.searchParams.get("hidden") !== null; + const stations = await trpc(event).station.list.query({ hidden }); + return { stations, hidden }; }); }; diff --git a/src/routes/(app)/visit/+page.svelte b/src/routes/(app)/visit/+page.svelte index 1ff17d6..8497c4e 100644 --- a/src/routes/(app)/visit/+page.svelte +++ b/src/routes/(app)/visit/+page.svelte @@ -4,9 +4,8 @@ import type { PageData } from "./$types"; import { URL_VISIT } from "$lib/shared/constants"; - import type { PaginationRequest } from "$lib/shared/model"; + import type { PaginationRequest, Station } from "$lib/shared/model"; import { trpc } from "$lib/shared/trpc"; - import type { RouterOutput } from "$lib/shared/trpc"; import { DateRange, getQueryUrl } from "$lib/shared/util"; import Autocomplete from "$lib/components/filter/Autocomplete.svelte"; @@ -17,7 +16,7 @@ export let data: PageData; let dateRange: DateRange; - let selection: RouterOutput["station"]["get"] | null; + let selection: Station | null; $: if (data.query.filter?.date) { const date = data.query.filter?.date[0]; diff --git a/tests/helpers/reset-db.ts b/tests/helpers/reset-db.ts index 5033ff4..a7d12f8 100644 --- a/tests/helpers/reset-db.ts +++ b/tests/helpers/reset-db.ts @@ -1,6 +1,8 @@ import { prisma } from "$lib/server/prisma"; -import { CATEGORIES, STATIONS, USERS } from "./testdata"; +import { + CATEGORIES, ROOMS, STATIONS, USERS, +} from "./testdata"; export default async () => { await prisma.$transaction([ @@ -18,11 +20,9 @@ export default async () => { prisma.category.createMany({ data: CATEGORIES }), prisma.station.createMany({ data: STATIONS }), prisma.room.createMany({ - data: [ - { id: 1, name: "R1.1", station_id: 1 }, - { id: 2, name: "R1.2", station_id: 1 }, - { id: 3, name: "R2.1", station_id: 2 }, - ], + data: ROOMS.map((v) => { + return { id: v.id, name: v.name, station_id: v.station.id }; + }), }), prisma.patient.createMany({ data: [ @@ -56,7 +56,7 @@ export default async () => { prisma.$executeRawUnsafe( `alter sequence stations_id_seq restart with ${STATIONS.length + 1}`, ), - prisma.$executeRawUnsafe("alter sequence rooms_id_seq restart with 4"), + prisma.$executeRawUnsafe(`alter sequence rooms_id_seq restart with ${ROOMS.length + 1}`), prisma.$executeRawUnsafe("alter sequence patients_id_seq restart with 4"), prisma.$executeRawUnsafe("alter sequence entry_executions_id_seq restart with 1"), prisma.$executeRawUnsafe("alter sequence entry_versions_id_seq restart with 1"), diff --git a/tests/helpers/testdata.ts b/tests/helpers/testdata.ts index e802b0e..1f7b0d2 100644 --- a/tests/helpers/testdata.ts +++ b/tests/helpers/testdata.ts @@ -1,9 +1,33 @@ -import type { Category, Station } from "$lib/shared/model"; +import type { Category, Station, Room } from "$lib/shared/model"; export const S1: Station = { id: 1, name: "S1" }; export const S2: Station = { id: 2, name: "S2" }; +export const S3: Station = { id: 3, name: "S3_tmp" }; -export const STATIONS: Station[] = [S1, S2]; +export const STATIONS: Station[] = [S1, S2, S3]; + +export const ROOMS: Room[] = [ + { + id: 1, + name: "R1.1", + station: S1, + }, + { + id: 2, + name: "R1.2", + station: S1, + }, + { + id: 3, + name: "R2.1", + station: S2, + }, + { + id: 4, + name: "Rtmp", + station: S2, + }, +]; export const CATEGORIES: Category[] = [ { diff --git a/tests/integration/query/category.ts b/tests/integration/query/category.ts index 2e4e0fc..2e17793 100644 --- a/tests/integration/query/category.ts +++ b/tests/integration/query/category.ts @@ -1,6 +1,8 @@ import { expect, test } from "vitest"; -import { getCategories, getCategory, newCategory } from "$lib/server/query"; +import { + deleteCategory, getCategories, getCategory, hideCategory, newCategory, +} from "$lib/server/query"; import { CATEGORIES } from "$tests/helpers/testdata"; test("create category", async () => { @@ -21,3 +23,19 @@ test("get categories", async () => { const categories = await getCategories(); expect(categories).toStrictEqual(CATEGORIES); }); + +test("delete categories", async () => { + await deleteCategory(6); + expect(getCategory(6)).rejects.toThrowError("No Category found"); +}); + +test("hide category", async () => { + await hideCategory(1, true); + const cs1 = await getCategories(); + const exp = [...CATEGORIES]; + exp.splice(0, 1); + expect(cs1).toStrictEqual(exp); + await hideCategory(1, false); + const cs2 = await getCategories(); + expect(cs2).toStrictEqual(CATEGORIES); +}); diff --git a/tests/integration/query/entry.ts b/tests/integration/query/entry.ts index b8c5e79..226075d 100644 --- a/tests/integration/query/entry.ts +++ b/tests/integration/query/entry.ts @@ -3,12 +3,14 @@ import { expect, test } from "vitest"; import { ErrorConflict } from "$lib/shared/util/error"; import { + getCategoryNEntries, getEntries, getEntry, getEntryNExecutions, getEntryNVersions, getEntryVersions, getNTodo, + getPatientNEntries, newEntry, newEntryExecution, newEntryVersion, @@ -21,6 +23,46 @@ const TEST_VERSION = { priority: false, }; +async function insertTestEntries() { + // Create some entries + const eId1 = await newEntry(1, { + patient_id: 1, + version: TEST_VERSION, + }); + const eId2 = await newEntry(1, { + patient_id: 2, + version: { + text: "Carrot cake jelly-o bonbon toffee chocolate.", + date: "2024-01-05", + priority: false, + category_id: null, + }, + }); + const eId3 = await newEntry(1, { + patient_id: 1, + version: { + text: "Cheesecake danish donut oat cake caramels.", + date: "2024-01-06", + priority: false, + category_id: null, + }, + }); + + // Update an entry + await newEntryVersion(2, eId1, { + category_id: 3, + text: `${TEST_VERSION.text}\n\n> Hello World`, + date: "2024-01-01", + priority: true, + }); + + // Execute entries + await newEntryExecution(1, eId2, { text: "Some execution txt" }); + await newEntryExecution(2, eId3, { text: "More execution txt" }); + + return { eId1, eId2, eId3 }; +} + test("create entry", async () => { const eId = await newEntry(1, { patient_id: 1, @@ -163,46 +205,6 @@ test("create entry execution (wrong old xid)", async () => { expect(async () => newEntryExecution(1, eId, { text: "x2" }, x1 + 1)).rejects.toThrowError(new ErrorConflict("old execution id does not match")); }); -async function insertTestEntries() { - // Create some entries - const eId1 = await newEntry(1, { - patient_id: 1, - version: TEST_VERSION, - }); - const eId2 = await newEntry(1, { - patient_id: 2, - version: { - text: "Carrot cake jelly-o bonbon toffee chocolate.", - date: "2024-01-05", - priority: false, - category_id: null, - }, - }); - const eId3 = await newEntry(1, { - patient_id: 1, - version: { - text: "Cheesecake danish donut oat cake caramels.", - date: "2024-01-06", - priority: false, - category_id: null, - }, - }); - - // Update an entry - await newEntryVersion(2, eId1, { - category_id: 3, - text: `${TEST_VERSION.text}\n\n> Hello World`, - date: "2024-01-01", - priority: true, - }); - - // Execute entries - await newEntryExecution(1, eId2, { text: "Some execution txt" }); - await newEntryExecution(2, eId3, { text: "More execution txt" }); - - return { eId1, eId2, eId3 }; -} - test("get entries", async () => { const { eId1, eId2, eId3 } = await insertTestEntries(); const entries = await getEntries({}, {}); @@ -292,3 +294,13 @@ test("get entries", async () => { const n = await getNTodo(new Date("2024-01-05")); expect(n).toBe(1); }); + +test("get patient n entries", async () => { + await insertTestEntries(); + expect(await getPatientNEntries(1)).toBe(2); +}); + +test("get category n entries", async () => { + await insertTestEntries(); + expect(await getCategoryNEntries(3)).toBe(1); +}); diff --git a/tests/integration/query/patient.ts b/tests/integration/query/patient.ts index 4b0ee39..e2633de 100644 --- a/tests/integration/query/patient.ts +++ b/tests/integration/query/patient.ts @@ -64,9 +64,7 @@ test("delete patient (restricted)", async () => { }, }); - expect(async () => deletePatient(pId)).rejects.toThrowError( - "cannot delete patient with entries", - ); + expect(async () => deletePatient(pId)).rejects.toThrowError(); }); test("hide patient", async () => { diff --git a/tests/integration/query/room.ts b/tests/integration/query/room.ts index d5ca5b0..b730a3f 100644 --- a/tests/integration/query/room.ts +++ b/tests/integration/query/room.ts @@ -1,42 +1,20 @@ import { expect, test } from "vitest"; -import type { Room, Station } from "$lib/shared/model"; +import type { RoomDetail } from "$lib/shared/model"; import { - deleteStation, + deleteRoom, getRoom, + getRoomNPatients, getRooms, getStation, - getStations, + hideRoom, newRoom, - newStation, updateStation, } from "$lib/server/query"; -import { S1, S2 } from "$tests/helpers/testdata"; - -test("create station", async () => { - const sId = await newStation({ name: "S3" }); - const station = await getStation(sId); - expect(station).toStrictEqual({ id: sId, name: "S3" } satisfies Station); -}); - -test("update station", async () => { - const name = "HelloStation"; - await updateStation(S1.id, { name }); - const station = await getStation(S1.id); - expect(station.id).toBe(S1.id); - expect(station.name).toBe(name); -}); - -test("delete station", async () => { - await deleteStation(S1.id); - expect(async () => getStation(S1.id)).rejects.toThrowError("No Station found"); -}); - -test("get stations", async () => { - const stations = await getStations(); - expect(stations).toStrictEqual([S1, S2]); -}); +import { + ROOMS, S1, +} from "$tests/helpers/testdata"; test("create room", async () => { const rId = await newRoom({ name: "A1", station_id: 1 }); @@ -45,7 +23,8 @@ test("create room", async () => { id: rId, name: "A1", station: S1, - } satisfies Room); + hidden: false, + } satisfies RoomDetail); }); test("update room", async () => { @@ -57,27 +36,26 @@ test("update room", async () => { }); test("delete room", async () => { - await deleteStation(S1.id); - expect(async () => getStation(S1.id)).rejects.toThrowError("No Station found"); + await deleteRoom(ROOMS[3].id); + expect(async () => getRoom(ROOMS[3].id)).rejects.toThrowError("No Room found"); +}); + +test("hide room", async () => { + await hideRoom(ROOMS[0].id, true); + const rs1 = await getRooms(); + const exp = [...ROOMS]; + exp.splice(0, 1); + expect(rs1).toStrictEqual(exp); + await hideRoom(ROOMS[0].id, false); + const rs2 = await getRooms(); + expect(rs2).toStrictEqual(ROOMS); }); test("get rooms", async () => { const rooms = await getRooms(); - expect(rooms).toStrictEqual([ - { - id: 1, - name: "R1.1", - station: S1, - }, - { - id: 2, - name: "R1.2", - station: S1, - }, - { - id: 3, - name: "R2.1", - station: S2, - }, - ] satisfies Room[]); + expect(rooms).toStrictEqual(ROOMS); +}); + +test("get room n patients", async () => { + expect(await getRoomNPatients(ROOMS[0].id)).toBe(1); }); diff --git a/tests/integration/query/station.ts b/tests/integration/query/station.ts new file mode 100644 index 0000000..65a8db9 --- /dev/null +++ b/tests/integration/query/station.ts @@ -0,0 +1,47 @@ +import { expect, test } from "vitest"; + +import type { StationDetail } from "$lib/shared/model"; + +import { + deleteStation, getStation, getStationNRooms, getStations, hideStation, newStation, updateStation, +} from "$lib/server/query"; +import { + STATIONS, S1, S2, S3, +} from "$tests/helpers/testdata"; + +test("create station", async () => { + const sId = await newStation({ name: "S3" }); + const station = await getStation(sId); + expect(station).toStrictEqual({ id: sId, name: "S3", hidden: false } satisfies StationDetail); +}); + +test("update station", async () => { + const name = "HelloStation"; + await updateStation(S1.id, { name }); + const station = await getStation(S1.id); + expect(station.id).toBe(S1.id); + expect(station.name).toBe(name); +}); + +test("delete station", async () => { + await deleteStation(S3.id); + expect(async () => getStation(S3.id)).rejects.toThrowError("No Station found"); +}); + +test("hide station", async () => { + await hideStation(S1.id, true); + const cs1 = await getStations(); + expect(cs1).toStrictEqual([S2, S3]); + await hideStation(S1.id, false); + const cs2 = await getStations(); + expect(cs2).toStrictEqual(STATIONS); +}); + +test("get stations", async () => { + const stations = await getStations(); + expect(stations).toStrictEqual(STATIONS); +}); + +test("get station n rooms", async () => { + expect(await getStationNRooms(1)).toBe(2); +}); From e6302f380b903e3ab3f4ef6b5c6e1483d2939027 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 6 May 2024 22:43:45 +0200 Subject: [PATCH 07/10] refactor: added return types --- src/app.d.ts | 2 +- src/lib/actions/outclick.ts | 6 ++-- src/lib/components/filter/Autocomplete.svelte | 32 +++++++++---------- src/lib/components/filter/FilterBar.svelte | 12 +++---- src/lib/components/filter/FilterChip.svelte | 10 +++--- .../components/filter/SavedFilterChip.svelte | 8 ++--- src/lib/components/filter/SavedFilters.svelte | 8 ++--- src/lib/components/filter/filters.ts | 4 ++- src/lib/components/table/CategoryField.svelte | 2 +- .../table/FilteredEntryTable.svelte | 8 ++--- .../table/FilteredPatientTable.svelte | 8 ++--- src/lib/components/table/PatientField.svelte | 2 +- src/lib/components/table/PatientTable.svelte | 2 +- src/lib/components/table/RoomField.svelte | 2 +- src/lib/components/table/SortHeader.svelte | 2 +- src/lib/components/table/UserField.svelte | 2 +- src/lib/components/ui/LoadingBar.svelte | 6 ++-- .../components/ui/PaginationButtons.svelte | 2 +- src/lib/components/ui/WeekSelector.svelte | 10 +++--- src/lib/server/auth.ts | 5 +-- src/lib/server/query/savedFilter.ts | 4 +-- src/lib/server/query/station.ts | 6 ++-- src/lib/server/query/util.ts | 14 ++++---- src/lib/server/trpc/context.ts | 3 +- src/lib/shared/model/validation.ts | 2 +- src/lib/shared/util/date.ts | 2 +- src/lib/shared/util/toast.ts | 4 +-- src/lib/shared/util/util.ts | 10 +++--- src/routes/(app)/visit/+page.svelte | 6 ++-- src/routes/login/+page.server.ts | 2 +- 30 files changed, 96 insertions(+), 90 deletions(-) diff --git a/src/app.d.ts b/src/app.d.ts index 0b9d41b..53b70ff 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -6,7 +6,7 @@ declare global { namespace App { // interface Error {} interface Locals { - session: Session; + session: Session | null; } // interface PageData {} // interface Platform {} diff --git a/src/lib/actions/outclick.ts b/src/lib/actions/outclick.ts index 8811649..1fc1dcd 100644 --- a/src/lib/actions/outclick.ts +++ b/src/lib/actions/outclick.ts @@ -1,5 +1,7 @@ -export default function clickOutside(node: Element) { - const handleClick = (event: MouseEvent) => { +import type { ActionReturn } from "svelte/action"; + +export default function clickOutside(node: Element): ActionReturn { + const handleClick = (event: MouseEvent): void => { const tnode = event.target as Element; if (!node.contains(tnode)) { diff --git a/src/lib/components/filter/Autocomplete.svelte b/src/lib/components/filter/Autocomplete.svelte index 304ed85..05badf5 100644 --- a/src/lib/components/filter/Autocomplete.svelte +++ b/src/lib/components/filter/Autocomplete.svelte @@ -59,9 +59,9 @@ /** Selection callback. Returns the new input value after selection */ export let onSelect: (item: T, kb: boolean) => OnSelectResult | void = () => {}; - export let onUnselect = () => {}; - export let onClose = (kb: boolean) => {}; - export let onBackspace = () => {}; + export let onUnselect = (): void => {}; + export let onClose = (kb: boolean): void => {}; + export let onBackspace = (): void => {}; let opened = false; let highlightIndex = 0; @@ -81,7 +81,7 @@ return ""; } - function setInputValue(v: string) { + function setInputValue(v: string): void { if (inputElm) inputElm.value = v; } @@ -108,7 +108,7 @@ return true; } - function markSelection() { + function markSelection(): void { if (selection) { if (selection.id) { const i = srcItems.findIndex((itm) => itm.id === selection?.id); @@ -126,7 +126,7 @@ highlight(); } - function clearSelection() { + function clearSelection(): void { selection = null; onUnselect(); setInputValue(""); @@ -134,14 +134,14 @@ updateSearch(); } - function onInput() { + function onInput(): void { selection = null; onUnselect(); opened = true; updateSearch(); } - function updateSearch() { + function updateSearch(): void { if (loadSrcItems()) { const searchWord = inputValue().toLowerCase().trim(); filteredItems = !selection && searchWord.length > 0 @@ -155,7 +155,7 @@ } } - export function open() { + export function open(): void { if (!opened) { updateSearch(); } @@ -163,14 +163,14 @@ if (inputElm) inputElm.focus(); } - export function close(kb = true) { + export function close(kb = true): void { if (opened) { onClose(kb); } opened = false; } - function selectListItem(item: T | undefined, kb: boolean) { + function selectListItem(item: T | undefined, kb: boolean): void { if (item) { selection = item; const selRes = onSelect(item, kb); @@ -185,7 +185,7 @@ } } - function onKeyDown(e: KeyboardEvent) { + function onKeyDown(e: KeyboardEvent): void { let { key } = e; if (key === "Tab" && e.shiftKey) key = "ShiftTab"; const fnmap: Record void> = { @@ -226,7 +226,7 @@ } } - function onKeyPress(e: KeyboardEvent) { + function onKeyPress(e: KeyboardEvent): void { if (e.key === "Enter") { if (opened) { selectItem(); @@ -235,7 +235,7 @@ } } - function onBlur() { + function onBlur(): void { if (!selection) { if (!noAutoselect1 && filteredItems.length === 1) { selectListItem(filteredItems[0], true); @@ -245,7 +245,7 @@ } } - function highlight() { + function highlight(): void { if (browser && opened) { window.setTimeout(() => { const query = ".selected"; @@ -264,7 +264,7 @@ } } - function selectItem() { + function selectItem(): void { const listItem = filteredItems[highlightIndex]; selectListItem(listItem, true); } diff --git a/src/lib/components/filter/FilterBar.svelte b/src/lib/components/filter/FilterBar.svelte index 6ad4925..32b7911 100644 --- a/src/lib/components/filter/FilterBar.svelte +++ b/src/lib/components/filter/FilterBar.svelte @@ -68,7 +68,7 @@ updateFromQueryData(filterData ?? {}); } - function updateFromQueryData(fd: FilterQdata) { + function updateFromQueryData(fd: FilterQdata): void { const filters: FilterData[] = []; for (const [id, value] of Object.entries(fd)) { // If filter is hidden or undefined, dont display it @@ -105,7 +105,7 @@ return new Set(); } - function focusInput() { + function focusInput(): void { if (autocomplete) autocomplete.open(); } @@ -166,7 +166,7 @@ return !valueless; } - function removeFilter(i: number) { + function removeFilter(i: number): void { const shouldUpdate = isFilterValueless(FILTERS[activeFilters[i].id].inputType) || activeFilters[i].selection !== null; activeFilters.splice(i, 1); @@ -174,15 +174,15 @@ if (shouldUpdate) updateFilter(); } - function updateFilter() { + function updateFilter(): void { onUpdate(getFilterQdata()); } - function onSearchInput(e: Event) { + function onSearchInput(e: Event): void { searchDebounce.trigger(); } - function onSearchKeypress(e: KeyboardEvent) { + function onSearchKeypress(e: KeyboardEvent): void { if (e.key === "Enter") { searchDebounce.now(); } diff --git a/src/lib/components/filter/FilterChip.svelte b/src/lib/components/filter/FilterChip.svelte index 2f8d7ec..1f76870 100644 --- a/src/lib/components/filter/FilterChip.svelte +++ b/src/lib/components/filter/FilterChip.svelte @@ -14,21 +14,21 @@ export let cache: Record = {}; export let fdata: FilterData; - export let onRemove = () => {}; - export let onSelection = (selection: SelectionOrText, kb: boolean) => {}; + export let onRemove = (): void => {}; + export let onSelection = (selection: SelectionOrText, kb: boolean): void => {}; let autocomplete: Autocomplete | undefined; - function startEditing() { + function startEditing(): void { fdata.editing = true; } - function stopEditing(kb = false) { + function stopEditing(kb = false): void { fdata.editing = false; if (fdata.selection) onSelection(fdata.selection, kb); } - function onClose(kb = false) { + function onClose(kb = false): void { if (fdata.selection) stopEditing(kb); else onRemove(); } diff --git a/src/lib/components/filter/SavedFilterChip.svelte b/src/lib/components/filter/SavedFilterChip.svelte index 61174c0..4e1594f 100644 --- a/src/lib/components/filter/SavedFilterChip.svelte +++ b/src/lib/components/filter/SavedFilterChip.svelte @@ -4,15 +4,15 @@ import Icon from "$lib/components/ui/Icon.svelte"; export let href = ""; - export let onSave = () => {}; - export let onRemove = () => {}; + export let onSave = (): void => {}; + export let onRemove = (): void => {}; - function onSaveInt(e: MouseEvent) { + function onSaveInt(e: MouseEvent): void { e.preventDefault(); onSave(); } - function onRemoveInt(e: MouseEvent) { + function onRemoveInt(e: MouseEvent): void { e.preventDefault(); onRemove(); } diff --git a/src/lib/components/filter/SavedFilters.svelte b/src/lib/components/filter/SavedFilters.svelte index 206f957..99364eb 100644 --- a/src/lib/components/filter/SavedFilters.svelte +++ b/src/lib/components/filter/SavedFilters.svelte @@ -20,7 +20,7 @@ return window.location.search.substring(1); } - function updateSavedFilter(filter: SavedFilter) { + function updateSavedFilter(filter: SavedFilter): void { savedFilters.update((v) => { if (!v[view]) v[view] = []; const ix = v[view].findIndex((f) => f.id === filter.id); @@ -34,7 +34,7 @@ }); } - function create() { + function create(): void { const query = getQuery(); if (query.length === 0) { toastInfo("Filter leer"); @@ -50,7 +50,7 @@ }).catch(toastError); } - function update(ix: number) { + function update(ix: number): void { const f = filters[ix]; const query = getQuery(); if (query.length === 0) { @@ -65,7 +65,7 @@ }).catch(toastError); } - function remove(ix: number) { + function remove(ix: number): void { const f = filters[ix]; trpc().savedFilter.delete.mutate(f.id).then(() => { toastInfo("Filter gelöscht"); diff --git a/src/lib/components/filter/filters.ts b/src/lib/components/filter/filters.ts index 7588de4..905e389 100644 --- a/src/lib/components/filter/filters.ts +++ b/src/lib/components/filter/filters.ts @@ -24,7 +24,9 @@ export function weekFilterItems(): BaseItem[] { const range = DateRange.thisWeek(); const res: BaseItem[] = []; - const addRange = (r: DateRange) => res.push({ id: r.toString(), name: r.format() }); + const addRange = (r: DateRange): void => { + res.push({ id: r.toString(), name: r.format() }); + }; addRange(new DateRange(null, range.start)); for (let i = 0; i < WEEK_LIMIT; i++) { diff --git a/src/lib/components/table/CategoryField.svelte b/src/lib/components/table/CategoryField.svelte index 3bb0505..8c438d7 100644 --- a/src/lib/components/table/CategoryField.svelte +++ b/src/lib/components/table/CategoryField.svelte @@ -13,7 +13,7 @@ ? colorToHex(getTextColor(hexToColor(category.color))) : null; - function onClick(e: MouseEvent) { + function onClick(e: MouseEvent): void { gotoEntityQuery( { filter: { diff --git a/src/lib/components/table/FilteredEntryTable.svelte b/src/lib/components/table/FilteredEntryTable.svelte index 6e5e389..fcb0e38 100644 --- a/src/lib/components/table/FilteredEntryTable.svelte +++ b/src/lib/components/table/FilteredEntryTable.svelte @@ -22,7 +22,7 @@ export let patientId: number | null = null; export let view: string | undefined = undefined; - function paginationUpdate(pagination: PaginationRequest) { + function paginationUpdate(pagination: PaginationRequest): void { updateQuery({ filter: query.filter, pagination, @@ -30,11 +30,11 @@ }); } - function filterUpdate(filter: FilterQdata | undefined) { + function filterUpdate(filter: FilterQdata | undefined): void { updateQuery({ filter, sort: query.sort }); } - function sortUpdate(sort: SortRequest | undefined) { + function sortUpdate(sort: SortRequest | undefined): void { updateQuery({ filter: query.filter, pagination: query.pagination, @@ -42,7 +42,7 @@ }); } - function updateQuery(q: typeof query) { + function updateQuery(q: typeof query): void { if (browser) { if (patientId !== null && q.filter?.patient) delete q.filter.patient; diff --git a/src/lib/components/table/FilteredPatientTable.svelte b/src/lib/components/table/FilteredPatientTable.svelte index 935a276..f87ac24 100644 --- a/src/lib/components/table/FilteredPatientTable.svelte +++ b/src/lib/components/table/FilteredPatientTable.svelte @@ -20,7 +20,7 @@ export let patients: RouterOutput["patient"]["list"]; export let baseUrl: string; - function paginationUpdate(pagination: PaginationRequest) { + function paginationUpdate(pagination: PaginationRequest): void { updateQuery({ filter: query.filter, pagination, @@ -28,11 +28,11 @@ }); } - function filterUpdate(filter: FilterQdata | undefined) { + function filterUpdate(filter: FilterQdata | undefined): void { updateQuery({ filter, sort: query.sort }); } - function sortUpdate(sort: SortRequest | undefined) { + function sortUpdate(sort: SortRequest | undefined): void { updateQuery({ filter: query.filter, pagination: query.pagination, @@ -40,7 +40,7 @@ }); } - function updateQuery(q: typeof query) { + function updateQuery(q: typeof query): void { if (browser) { // Update page URL const url = getQueryUrl(q, baseUrl); diff --git a/src/lib/components/table/PatientField.svelte b/src/lib/components/table/PatientField.svelte index 296c797..c3480b5 100644 --- a/src/lib/components/table/PatientField.svelte +++ b/src/lib/components/table/PatientField.svelte @@ -5,7 +5,7 @@ export let patient: RouterOutput["patient"]["list"]["items"][0]; export let baseUrl: string; - function onClick(e: MouseEvent) { + function onClick(e: MouseEvent): void { gotoEntityQuery( { filter: { diff --git a/src/lib/components/table/PatientTable.svelte b/src/lib/components/table/PatientTable.svelte index 868df6c..8df3916 100644 --- a/src/lib/components/table/PatientTable.svelte +++ b/src/lib/components/table/PatientTable.svelte @@ -1,5 +1,5 @@ + +
+

Visitenbuch

+

Version: {version}

+

Letzte Änderung: {lastmod}

+

Open-Source-Lizenzen

+
diff --git a/vite.config.js b/vite.config.js index 49c9e7c..6539b16 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,77 @@ +import { exec } from "child_process"; +import { promisify } from "util"; + import { sveltekit } from "@sveltejs/kit/vite"; +import { createViteLicensePlugin } from "rollup-license-plugin"; import { defineConfig } from "vitest/config"; +// Get current tag/commit and last commit date from git +const pexec = promisify(exec); +let [version, lastmod] = ( + await Promise.allSettled([ + pexec("git describe --tags || git rev-parse --short HEAD"), + pexec('git log -1 --format=%cd --date=format:"%Y-%m-%d %H:%M"'), + ]) +).map((v) => { + if (v.status !== "fulfilled") throw Error("could not get version info from git"); + return JSON.stringify(v.value.stdout.trim()); +}); + export default defineConfig({ - plugins: [sveltekit()], + plugins: [ + sveltekit(), + createViteLicensePlugin({ + additionalFiles: { + "oss-licenses.html": (packages) => { + let res = ` + +Visitenbuch - Lizenzen + + +

Open-Source-Lizenzen

+JSON-formatted license list +`; + for (const p of packages) { + // @ts-expect-error repo not present in type definition + let rp = p.repository; + // @ts-expect-error author not present in type definition + let aut = p.author; + if (typeof aut === "object") { + aut = aut.name; + } + + let repoUrl = null; + if (typeof rp === "string") { + if (rp.startsWith("http")) repoUrl = rp; + else if (rp.startsWith("git+http")) repoUrl = rp.substring(4); + else if (rp.startsWith("git://")) repoUrl = "https://" + rp.substring(6); + else if (rp.startsWith("github:")) repoUrl = "https://github.com/" + rp.substring(7); + else if (rp.match(/^[\w-]+\/[\w-]+$/)) repoUrl = "https://github.com/" + rp; + } + + res += `
\n`; + res += `

${p.name}

\n`; + res += `\n`; + res += `\n`; + if (aut) res += `\n`; + res += `\n`; + if (repoUrl) res += `\n`; + else if (rp) res += `\n`; + res += `
Version:${p.version}
Author:${aut}
License:${p.license}
Repository:${repoUrl}
Repository:${rp}
\n`; + res += "
"; + } + + res += "\n\n"; + return res; + }, + }, + }), + ], test: { include: ["src/**/*.{test,spec}.{js,ts}"], }, + define: { + __VERSION__: version, + __LASTMOD__: lastmod, + }, }); From 1b96a46dcf2b34318bed5fc383a839a6d7cfd25e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 7 May 2024 00:44:42 +0200 Subject: [PATCH 09/10] chore: add git-cliff --- CHANGELOG.md | 10 ++++++ cliff.toml | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- release.sh | 27 ++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 cliff.toml create mode 100755 release.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..00942c2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + + +## [v0.1.0](https://code.thetadev.de/HSA/Visitenbuch/commits/tag/v0.1.0) - 2024-05-06 + +Initial release + + diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..8593819 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,100 @@ +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration +# +# Lines starting with "#" are comments. +# Configuration options are organized into tables and keys. +# See documentation for more information on available options. + +[changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% set repo_url = "https://code.thetadev.de/HSA/Visitenbuch" %}\ +{% if version %}\ + {%set vname = version | split(pat="/") | last %} + {%if previous.version %}\ + ## [{{ vname }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\ + {% else %}\ + ## [{{ vname }}]({{ repo_url }}/commits/tag/{{ version }})\ + {% endif %} - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% if previous.version %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }} - \ + ([{{ commit.id | truncate(length=7, end="") }}]({{ repo_url }}/commit/{{ commit.id }}))\ + {% endfor %} +{% endfor %}\ +{% else %} +Initial release +{% endif %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing s +trim = true +# postprocessors +postprocessors = [ + # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL +] + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Replace issue numbers + #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, + # Check spelling of the commit with https://github.com/crate-ci/typos + # If the spelling is incorrect, it will be automatically fixed. + #{ pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "🚀 Features" }, + { message = "^fix", group = "🐛 Bug Fixes" }, + { message = "^doc", group = "📚 Documentation" }, + { message = "^perf", group = "⚡ Performance" }, + { message = "^refactor", group = "🚜 Refactor" }, + { message = "^style", group = "🎨 Styling" }, + { message = "^test", group = "🧪 Testing" }, + { message = "^chore\\(release\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore", group = "⚙️ Miscellaneous Tasks" }, + { message = "^ci", skip = true }, + { body = ".*security", group = "🛡️ Security" }, + { message = "^revert", group = "◀️ Revert" }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = false +# regex for matching git tags +# tag_pattern = "v[0-9].*" +# regex for skipping tags +# skip_tags = "" +# regex for ignoring tags +# ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" +# limit the number of commits included in the changelog. +# limit_commits = 42 diff --git a/package.json b/package.json index ff549ca..fbb40e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "visitenbuch", - "version": "0.0.1", + "version": "0.2.0", "private": true, "license": "AGPL-3.0", "scripts": { diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..06abc7b --- /dev/null +++ b/release.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -e + +CHANGELOG="CHANGELOG.md" + +VERSION=$(jq -r '.version' package.json) +TAG="v${VERSION}" +echo "Releasing $TAG:" + +if git rev-parse "$TAG" >/dev/null 2>&1; then echo "version tag $TAG already exists"; exit 1; fi + +CLIFF_ARGS="--tag '${TAG}' --unreleased" +echo "git-cliff $CLIFF_ARGS" +if [ -f "$CHANGELOG" ]; then + eval "git-cliff $CLIFF_ARGS --prepend '$CHANGELOG'" +else + eval "git-cliff $CLIFF_ARGS --output '$CHANGELOG'" +fi + +editor "$CHANGELOG" + +git add "$CHANGELOG" +git commit -m "chore(release): release v$VERSION" + +awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CHANGELOG" | git tag -as -F - --cleanup whitespace "$TAG" + +echo "🚀 Run 'git push origin $TAG' to publish" From 0ae735f2860919dffb957dbc7f556865cd73237e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 7 May 2024 00:55:01 +0200 Subject: [PATCH 10/10] chore(release): release v0.2.0 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00942c2..595addb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,37 @@ All notable changes to this project will be documented in this file. +## [v0.2.0](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.1.0..v0.2.0) - 2024-05-06 + +### 🚀 Features + +- Add toast error messages - ([80c6243](https://code.thetadev.de/HSA/Visitenbuch/commit/80c6243e2b1fe1c8c92b57809c094219be666065)) +- Improve week selector, select all TODO items by default - ([21f145f](https://code.thetadev.de/HSA/Visitenbuch/commit/21f145f5f08e4f44beecd3d373dfc69e5fc94f23)) +- Use global store for saved filters, add default filters - ([f36ae71](https://code.thetadev.de/HSA/Visitenbuch/commit/f36ae71d32ebcfc5054dca9c6907ffc1554f39e3)) +- Hide rooms/stations/categories - ([2cb8dce](https://code.thetadev.de/HSA/Visitenbuch/commit/2cb8dce51a880d46af3a77ee9cbdfce680b88755)) +- Add about page, licenses - ([5230ee3](https://code.thetadev.de/HSA/Visitenbuch/commit/5230ee375c7b25e577dc2ae045309d8cd54cfdf4)) + +### 🐛 Bug Fixes + +- Date timezone issue, refactor utils - ([efb0e24](https://code.thetadev.de/HSA/Visitenbuch/commit/efb0e246127489d618ca6e3ea50f10dd1140a954)) +- Normalize line endings - ([799dbc4](https://code.thetadev.de/HSA/Visitenbuch/commit/799dbc4097b5cf72e813e67b6ca6ccfe6ab5f08b)) +- Light/dark theme - ([8630b6a](https://code.thetadev.de/HSA/Visitenbuch/commit/8630b6a32d850e93ff1b426a92f68400f7b2f666)) +- Not using UTC dates for parsing date ranges in backend - ([519ae01](https://code.thetadev.de/HSA/Visitenbuch/commit/519ae01eeec294f7522c5ad9bcf8f2b6b44539fb)) + +### 🚜 Refactor + +- Added return types - ([e6302f3](https://code.thetadev.de/HSA/Visitenbuch/commit/e6302f380b903e3ab3f4ef6b5c6e1483d2939027)) + +### ⚙️ Miscellaneous Tasks + +- Update dependencies - ([e3f7341](https://code.thetadev.de/HSA/Visitenbuch/commit/e3f7341a0e575a9b0cb922c9b697c21f6f6875c6)) +- Add git-cliff - ([1b96a46](https://code.thetadev.de/HSA/Visitenbuch/commit/1b96a46dcf2b34318bed5fc383a839a6d7cfd25e)) + +### Build + +- Remove docker multistage build, add entrypoint - ([be52e70](https://code.thetadev.de/HSA/Visitenbuch/commit/be52e70e8d5abed676c9594f2de0e749b7d2da08)) + + ## [v0.1.0](https://code.thetadev.de/HSA/Visitenbuch/commits/tag/v0.1.0) - 2024-05-06 Initial release