Compare commits

..

No commits in common. "cfe5fc2d9f012aafc8aa470a5b2f8d1366d29dcc" and "152824f6c071b4f647be90d80c680cbd8ce736f3" have entirely different histories.

43 changed files with 110 additions and 649 deletions

View file

@ -1,10 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View file

@ -1,36 +0,0 @@
# stage build
FROM node:20-alpine
WORKDIR /app
# copy everything to the container
COPY . .
# clean install all dependencies
RUN corepack enable && pnpm i
# remove potential security issues
RUN pnpm audit fix
# build SvelteKit appmakeAuthjsRequest
RUN pnpm run build
# stage run
FROM node:20-alpine
WORKDIR /app
# copy dependency list
COPY --from=0 /app/package*.json /app/pnpm-lock.yaml ./
COPY --from=0 /app/patches ./patches
COPY --from=0 /app/prisma ./prisma
# Setup pnpm, install Prisma CLI (for generating client) and install dependencies
RUN corepack enable && pnpm i --prod && pnpm audit fix && npx prisma generate
# copy built SvelteKit app to /app
COPY --from=0 /app/build ./
EXPOSE 3000
CMD ["node", "./index.js"]

View file

@ -415,6 +415,18 @@ export default [
}, },
], ],
// disallow dangling underscores in identifiers
// https://eslint.org/docs/rules/no-underscore-dangle
"no-underscore-dangle": [
"warn",
{
allow: [],
allowAfterThis: false,
allowAfterSuper: false,
enforceInMethodNames: true,
},
],
// disallow the use of Boolean literals in conditional expressions // disallow the use of Boolean literals in conditional expressions
// also, prefer `a || b` over `a ? a : b` // also, prefer `a || b` over `a ? a : b`
// https://eslint.org/docs/rules/no-unneeded-ternary // https://eslint.org/docs/rules/no-unneeded-ternary

View file

@ -21,10 +21,9 @@
"@floating-ui/core": "^1.6.0", "@floating-ui/core": "^1.6.0",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@prisma/client": "^5.12.1", "@prisma/client": "^5.12.1",
"carta-md": "4.0.2", "carta-md": "^4.0.0",
"diff": "^5.2.0", "diff": "^5.2.0",
"isomorphic-dompurify": "^2.7.0", "isomorphic-dompurify": "^2.7.0",
"prisma": "^5.12.1",
"qs": "^6.12.1", "qs": "^6.12.1",
"set-cookie-parser": "^2.6.0", "set-cookie-parser": "^2.6.0",
"svelte-floating-ui": "^1.5.8", "svelte-floating-ui": "^1.5.8",
@ -57,6 +56,7 @@
"globals": "^15.0.0", "globals": "^15.0.0",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.1", "postcss-nesting": "^12.1.1",
"prisma": "^5.12.1",
"svelte": "^4.2.15", "svelte": "^4.2.15",
"svelte-check": "^3.6.9", "svelte-check": "^3.6.9",
"sveltekit-superforms": "^2.12.5", "sveltekit-superforms": "^2.12.5",
@ -72,7 +72,7 @@
"type": "module", "type": "module",
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"carta-md@4.0.2": "patches/carta-md@4.0.2.patch" "carta-md@4.0.0": "patches/carta-md@4.0.0.patch"
} }
} }
} }

View file

@ -5,9 +5,9 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
patchedDependencies: patchedDependencies:
carta-md@4.0.2: carta-md@4.0.0:
hash: i33ea43vfgrg3ziu25cfu7s2zq hash: 4dz4dhp4dewr3xkzk2rwum2jdq
path: patches/carta-md@4.0.2.patch path: patches/carta-md@4.0.0.patch
dependencies: dependencies:
'@auth/core': '@auth/core':
@ -23,8 +23,8 @@ dependencies:
specifier: ^5.12.1 specifier: ^5.12.1
version: 5.12.1(prisma@5.12.1) version: 5.12.1(prisma@5.12.1)
carta-md: carta-md:
specifier: 4.0.2 specifier: ^4.0.0
version: 4.0.2(patch_hash=i33ea43vfgrg3ziu25cfu7s2zq)(svelte@4.2.15) version: 4.0.0(patch_hash=4dz4dhp4dewr3xkzk2rwum2jdq)(svelte@4.2.15)
diff: diff:
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.2.0 version: 5.2.0
@ -1075,8 +1075,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@shikijs/core@1.4.0: /@shikijs/core@1.3.0:
resolution: {integrity: sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==} resolution: {integrity: sha512-7fedsBfuILDTBmrYZNFI8B6ATTxhQAasUHllHmjvSZPnoq4bULWoTpHwmuQvZ8Aq03/tAa2IGo6RXqWtHdWaCA==}
dev: false dev: false
/@sideway/address@4.1.5: /@sideway/address@4.1.5:
@ -1935,8 +1935,8 @@ packages:
resolution: {integrity: sha512-19NuN1/3PjA3QI8Eki55N8my4LzfkMCRLgCVfrl/slbSAchQfV0+GwjPrK3rq37As4UCLlM/DHajbKkAqbv92Q==} resolution: {integrity: sha512-19NuN1/3PjA3QI8Eki55N8my4LzfkMCRLgCVfrl/slbSAchQfV0+GwjPrK3rq37As4UCLlM/DHajbKkAqbv92Q==}
dev: true dev: true
/carta-md@4.0.2(patch_hash=i33ea43vfgrg3ziu25cfu7s2zq)(svelte@4.2.15): /carta-md@4.0.0(patch_hash=4dz4dhp4dewr3xkzk2rwum2jdq)(svelte@4.2.15):
resolution: {integrity: sha512-wMlw0r5RZiVwvF3dyxE/vHj9pXlXbzpijJ3m/o9zqZe7Cf6D96AjyBHBpa0A0OPj/uEJVF3k0R6ctopBJCpCQg==} resolution: {integrity: sha512-mxIoN3dqcjgv8i5FIUBH69lclx8A1/FB/FaFymWBzKE4AvUdy/X6VQGBNzAO3ybSAdceMT0RrAhY5/KnoFI8Hg==}
peerDependencies: peerDependencies:
svelte: ^3.54.0 || ^4.0.0 svelte: ^3.54.0 || ^4.0.0
dependencies: dependencies:
@ -1944,7 +1944,7 @@ packages:
remark-gfm: 4.0.0 remark-gfm: 4.0.0
remark-parse: 11.0.0 remark-parse: 11.0.0
remark-rehype: 11.1.0 remark-rehype: 11.1.0
shiki: 1.4.0 shiki: 1.3.0
svelte: 4.2.15 svelte: 4.2.15
unified: 11.0.4 unified: 11.0.4
transitivePeerDependencies: transitivePeerDependencies:
@ -4860,10 +4860,10 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/shiki@1.4.0: /shiki@1.3.0:
resolution: {integrity: sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==} resolution: {integrity: sha512-9aNdQy/etMXctnPzsje1h1XIGm9YfRcSksKOGqZWXA/qP9G18/8fpz5Bjpma8bOgz3tqIpjERAd6/lLjFyzoww==}
dependencies: dependencies:
'@shikijs/core': 1.4.0 '@shikijs/core': 1.3.0
dev: false dev: false
/side-channel@1.0.6: /side-channel@1.0.6:

View file

@ -4,9 +4,11 @@
@tailwind utilities; @tailwind utilities;
@layer components { @layer components {
.btn-animation { .btn-transition {
@apply duration-200 ease-out; @apply duration-200 ease-out;
animation: button-pop var(--animation-btn, 0.25s) ease-out; animation: button-pop var(--animation-btn, 0.25s) ease-out;
transition-property: color, background-color, border-color, opacity, box-shadow,
transform;
&:active:hover, &:active:hover,
&:active:focus { &:active:focus {
animation: button-pop 0s ease-out; animation: button-pop 0s ease-out;
@ -45,7 +47,7 @@ button {
@apply border border-solid border-base-content/30; @apply border border-solid border-base-content/30;
.row { .row {
@apply flex; @apply flex flex-row;
} }
.row, .row,
@ -74,12 +76,3 @@ button {
@apply bg-primary text-primary-content py-1; @apply bg-primary text-primary-content py-1;
} }
} }
.card-animation {
@apply duration-200 ease-out;
&:active:hover,
&:active:focus {
animation: button-pop 0s ease-out;
transform: scale(0.99);
}
}

View file

@ -31,7 +31,3 @@ export const handle = sequence(
authorization, authorization,
createTRPCHandle({ router, createContext }), createTRPCHandle({ router, createContext }),
); );
// Allow server application to exit
process.on("SIGINT", process.exit); // Ctrl+C
process.on("SIGTERM", process.exit); // docker stop

View file

@ -36,7 +36,7 @@
</a> </a>
</Header> </Header>
<p class="text-sm flex gap-2"> <p class="text-sm flex flex-row gap-2">
<span>Erstellt {humanDate(entry.created_at, true)}</span> <span>Erstellt {humanDate(entry.created_at, true)}</span>
<span>·</span> <span>·</span>
{#if entry.execution} {#if entry.execution}

View file

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { RouterOutput } from "$lib/shared/trpc"; import type { RouterOutput } from "$lib/shared/trpc";
import { formatPatientName } from "$lib/shared/util";
import RoomField from "$lib/components/table/RoomField.svelte"; import RoomField from "$lib/components/table/RoomField.svelte";
@ -14,7 +13,9 @@
<RoomField room={patient.room} /> <RoomField room={patient.room} />
{/if} {/if}
<a href="/patient/{patient.id}"> <a href="/patient/{patient.id}">
{formatPatientName(patient)} {patient.first_name}
{patient.last_name}
({patient.age})
</a> </a>
</div> </div>
</div> </div>

View file

@ -1,17 +1,4 @@
<script lang="ts"> <script lang="ts">
import { browser } from "$app/environment";
import { mdiClose } from "@mdi/js";
import { onMount } from "svelte";
import { createFloatingActions } from "svelte-floating-ui";
import { shift } from "svelte-floating-ui/dom";
import Icon from "$lib/components/ui/Icon.svelte";
import IconButton from "$lib/components/ui/IconButton.svelte";
import outclick from "$lib/actions/outclick";
import type { BaseItem } from "./types";
/** /**
* This is a simplified version of simple-svelte-autocomplete * This is a simplified version of simple-svelte-autocomplete
* Original source: https://github.com/pstanoev/simple-svelte-autocomplete * Original source: https://github.com/pstanoev/simple-svelte-autocomplete
@ -19,6 +6,17 @@
* MIT License * MIT License
*/ */
import { browser } from "$app/environment";
import { onMount } from "svelte";
import { createFloatingActions } from "svelte-floating-ui";
import { shift } from "svelte-floating-ui/dom";
import Icon from "$lib/components/ui/Icon.svelte";
import outclick from "$lib/actions/outclick";
import type { BaseItem } from "./types";
type T = $$Generic<BaseItem>; type T = $$Generic<BaseItem>;
type OnSelectResult = { newValue: string; close: boolean }; type OnSelectResult = { newValue: string; close: boolean };
@ -50,7 +48,6 @@
/** Name of the form input. This sets a hidden field to the ID of the selected element /** Name of the form input. This sets a hidden field to the ID of the selected element
* (to allow Svelte Superforms to work) */ * (to allow Svelte Superforms to work) */
export let idInputName: string | undefined = undefined; export let idInputName: string | undefined = undefined;
export let clearBtn = false;
/** Function to filter shown items */ /** Function to filter shown items */
export let filterFn: (item: T) => boolean = () => true; export let filterFn: (item: T) => boolean = () => true;
@ -281,7 +278,7 @@
} }
</script> </script>
<div class="flex-grow {cls}" class:relative={clearBtn} on:outclick={close} use:outclick> <div class="flex-grow {cls}" on:outclick={close} use:outclick>
<input <input
bind:this={inputElm} bind:this={inputElm}
class={inputCls} class={inputCls}
@ -332,12 +329,6 @@
<!-- Hidden input field (acting as form input) --> <!-- Hidden input field (acting as form input) -->
<input name={idInputName} type="hidden" value={selection?.id ?? ""} /> <input name={idInputName} type="hidden" value={selection?.id ?? ""} />
{/if} {/if}
{#if clearBtn && selection}
<div class="absolute bottom-0 right-0 h-full flex items-center">
<IconButton cls="" path={mdiClose} on:click={clearSelection} />
</div>
{/if}
</div> </div>
<style lang="postcss"> <style lang="postcss">

View file

@ -4,7 +4,7 @@
import { Debouncer } from "$lib/shared/util"; import { Debouncer } from "$lib/shared/util";
import IconButton from "$lib/components/ui/IconButton.svelte"; import Icon from "$lib/components/ui/Icon.svelte";
import Autocomplete from "./Autocomplete.svelte"; import Autocomplete from "./Autocomplete.svelte";
import EntryFilterChip from "./FilterChip.svelte"; import EntryFilterChip from "./FilterChip.svelte";
@ -16,7 +16,7 @@
BaseItem, BaseItem,
SelectionOrText, SelectionOrText,
} from "./types"; } from "./types";
import { InputType, isFilterValueless } from "./types"; import { isFilterValueless } from "./types";
/** Filter definitions */ /** Filter definitions */
export let FILTERS: { [key: string]: FilterDef }; export let FILTERS: { [key: string]: FilterDef };
@ -28,7 +28,7 @@
export let hiddenFilters: string[] = []; export let hiddenFilters: string[] = [];
/** True if a separate search field should be displayed */ /** True if a separate search field should be displayed */
export let search = false; export let search = false;
export let view: string | undefined = undefined; export let view: string;
let autocomplete: Autocomplete<BaseItem> | undefined; let autocomplete: Autocomplete<BaseItem> | undefined;
let activeFilters: FilterData[] = []; let activeFilters: FilterData[] = [];
@ -116,24 +116,26 @@
const key = filter.id; const key = filter.id;
let val = null; let val = null;
if (filter.inputType === InputType.None) { if (filter.inputType === 0) {
// Valueless filter (val = true) // Valueless filter (val = true)
val = true; val = true;
} else if (filter.inputType === InputType.FreeText) { } else if (filter.inputType === 1) {
// Text input // Text input
val = fdata.selection?.name; val = fdata.selection?.name;
} else if (filter.inputType === InputType.FilterList && fdata.selection) { } else if (filter.inputType === 2 && fdata.selection) {
// Filter list // Filter list
val = [{ id: fdata.selection.id!, name: fdata.selection.name }]; // @ts-expect-error TODO
} else if (filter.inputType === InputType.Boolean) { val = { id: fdata.selection.id, name: fdata.selection.name };
} else if (filter.inputType === 3) {
val = Boolean(fdata.selection?.toggle); val = Boolean(fdata.selection?.toggle);
} }
if (val !== null && val !== undefined) { if (val !== null && val !== undefined) {
if (filter.inputType === 2) { if (filter.inputType === 2) {
// @ts-expect-error fd[key] is checked // @ts-expect-error fd[key] is checked
if (Array.isArray(fd[key])) fd[key].push(...val); if (Array.isArray(fd[key])) fd[key].push(val);
else fd[key] = val; // @ts-expect-error fd[key] is checked
else fd[key] = [val];
} else { } else {
fd[key] = val; fd[key] = val;
} }
@ -221,15 +223,17 @@
partOfFilterbar partOfFilterbar
placeholder="Filter" placeholder="Filter"
/> />
<IconButton <button
ariaLabel="Alle Filter entfernen" class="btn btn-sm btn-circle btn-ghost absolute bottom-0 right-0"
cls="absolute bottom-0 right-0" aria-label="Alle Filter entfernen"
path={mdiClose}
on:click={() => { on:click={() => {
activeFilters = []; activeFilters = [];
searchVal = ""; searchVal = "";
updateFilter(); updateFilter();
}} /> }}
>
<Icon path={mdiClose} size={1.2} />
</button>
</div> </div>
{#if search} {#if search}
<input <input
@ -244,9 +248,7 @@
<slot /> <slot />
</div> </div>
{#if view}
<SavedFilters {view} /> <SavedFilters {view} />
{/if}
<style lang="postcss"> <style lang="postcss">
.filterbar-outer { .filterbar-outer {

View file

@ -4,9 +4,8 @@
import Icon from "$lib/components/ui/Icon.svelte"; import Icon from "$lib/components/ui/Icon.svelte";
import Autocomplete from "./Autocomplete.svelte"; import Autocomplete from "./Autocomplete.svelte";
import { import type {
InputType, BaseItem, FilterData, FilterDef, SelectionOrText,
type BaseItem, type FilterData, type FilterDef, type SelectionOrText,
} from "./types"; } from "./types";
export let filter: FilterDef; export let filter: FilterDef;
@ -37,7 +36,7 @@
autocomplete.open(); autocomplete.open();
} }
const TOFF = " aus"; // Toggle name suffix (in case toggleOff.name is unset) const TOFF = " aus";
$: toggleState = fdata.selection?.toggle !== false; $: toggleState = fdata.selection?.toggle !== false;
$: filterName = toggleState $: filterName = toggleState
@ -73,7 +72,7 @@
{#if hasInputField} {#if hasInputField}
{#if fdata.editing} {#if fdata.editing}
{#if filter.inputType === InputType.FilterList} {#if filter.inputType === 2}
{@const hids = hiddenIds()} {@const hids = hiddenIds()}
<Autocomplete <Autocomplete
bind:this={autocomplete} bind:this={autocomplete}
@ -84,8 +83,8 @@
onBackspace={onRemove} onBackspace={onRemove}
{onClose} {onClose}
onSelect={(item) => { onSelect={(item) => {
// Accept the selection if the user selected a variant // Accept the selection if this is a free text field or the user selected a variant
if (item.id) { if (filter.inputType !== 2 || item.id) {
fdata.selection = item; fdata.selection = item;
return { close: true, newValue: "" }; return { close: true, newValue: "" };
} }

View file

@ -18,7 +18,7 @@
} }
</script> </script>
<a class="chip btn-animation" {href}> <a class="chip btn-transition" {href}>
<span class="mx-2"> <span class="mx-2">
<slot /> <slot />
</span> </span>

View file

@ -56,7 +56,7 @@
} }
</script> </script>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-row flex-wrap items-center gap-2">
<div class="text-sm h-8 flex items-center"> <div class="text-sm h-8 flex items-center">
Gespeicherte Filter: Gespeicherte Filter:
</div> </div>

View file

@ -4,7 +4,6 @@ import {
mdiAccountMultipleOutline, mdiAccountMultipleOutline,
mdiAccountRemoveOutline, mdiAccountRemoveOutline,
mdiBedKingOutline, mdiBedKingOutline,
mdiCalendar,
mdiCheckboxBlankOutline, mdiCheckboxBlankOutline,
mdiCheckboxOutline, mdiCheckboxOutline,
mdiDoctor, mdiDoctor,
@ -14,75 +13,58 @@ import {
mdiTag, mdiTag,
} from "@mdi/js"; } from "@mdi/js";
import { WEEK_LIMIT } from "$lib/shared/constants";
import { trpc } from "$lib/shared/trpc"; import { trpc } from "$lib/shared/trpc";
import { DateRange, dateToYMD } from "$lib/shared/util";
import { type FilterDef, InputType, type BaseItem } from "./types"; import type { FilterDef } from "./types";
export function weekFilterItems(earlierLater: boolean): BaseItem[] {
const range = DateRange.thisWeek();
const res = [];
if (earlierLater) res.push({ id: ".." + dateToYMD(range.start!), name: "Früher" });
for (let i = 0; i < WEEK_LIMIT; i++) {
res.push({ id: range.toString(), name: range.format() });
range.addDays(7);
}
if (earlierLater) res.push({ id: dateToYMD(range.start!) + "..", name: "Später" });
return res;
}
export const ENTRY_FILTERS: { [key: string]: FilterDef } = { export const ENTRY_FILTERS: { [key: string]: FilterDef } = {
category: { category: {
id: "category", id: "category",
name: "Kategorie", name: "Kategorie",
icon: mdiTag, icon: mdiTag,
inputType: InputType.FilterList, inputType: 2,
options: async () => trpc().category.list.query(), options: async () => trpc().category.list.query(),
}, },
author: { author: {
id: "author", id: "author",
name: "Autor", name: "Autor",
icon: mdiAccount, icon: mdiAccount,
inputType: InputType.FilterList, inputType: 2,
options: async () => trpc().user.getNames.query(), options: async () => trpc().user.getNames.query(),
}, },
executor: { executor: {
id: "executor", id: "executor",
name: "Erledigt von", name: "Erledigt von",
icon: mdiDoctor, icon: mdiDoctor,
inputType: InputType.FilterList, inputType: 2,
options: async () => trpc().user.getNames.query(), options: async () => trpc().user.getNames.query(),
}, },
patient: { patient: {
id: "patient", id: "patient",
name: "Patient", name: "Patient",
icon: mdiAccountInjury, icon: mdiAccountInjury,
inputType: InputType.FilterList, inputType: 2,
options: async () => trpc().patient.getNames.query(), options: async () => trpc().patient.getNames.query(),
}, },
station: { station: {
id: "station", id: "station",
name: "Station", name: "Station",
icon: mdiDomain, icon: mdiDomain,
inputType: InputType.FilterList, inputType: 2,
options: async () => trpc().station.list.query(), options: async () => trpc().station.list.query(),
}, },
room: { room: {
id: "room", id: "room",
name: "Zimmer", name: "Zimmer",
icon: mdiBedKingOutline, icon: mdiBedKingOutline,
inputType: InputType.FilterList, inputType: 2,
options: async () => trpc().room.list.query(), options: async () => trpc().room.list.query(),
}, },
done: { done: {
id: "done", id: "done",
name: "Erledigt", name: "Erledigt",
icon: mdiCheckboxOutline, icon: mdiCheckboxOutline,
inputType: InputType.Boolean, inputType: 3,
toggleOff: { toggleOff: {
name: "Zu erledigen", name: "Zu erledigen",
icon: mdiCheckboxBlankOutline, icon: mdiCheckboxBlankOutline,
@ -92,20 +74,13 @@ export const ENTRY_FILTERS: { [key: string]: FilterDef } = {
id: "priority", id: "priority",
name: "Priorität", name: "Priorität",
icon: mdiExclamation, icon: mdiExclamation,
inputType: InputType.None, inputType: 0,
}, },
search: { search: {
id: "search", id: "search",
name: "Beschreibung", name: "Beschreibung",
icon: mdiMagnify, icon: mdiMagnify,
inputType: InputType.FreeText, inputType: 1,
},
date: {
id: "date",
name: "Woche",
icon: mdiCalendar,
inputType: InputType.FilterList,
options: async () => weekFilterItems(true),
}, },
}; };

View file

@ -1,10 +1,3 @@
export enum InputType {
None = 0,
FreeText = 1,
FilterList = 2,
Boolean = 3,
}
export type BaseItem = { export type BaseItem = {
id: string | number; id: string | number;
name: string; name: string;
@ -22,7 +15,8 @@ export type FilterDef = {
id: string; id: string;
name: string; name: string;
icon?: string; icon?: string;
inputType: InputType; // 0: No input (value: true); 1: Free text input; 2: Filter list; 3: Boolean switch
inputType: 0 | 1 | 2 | 3;
toggleOff?: { toggleOff?: {
name: string; name: string;
icon?: string; icon?: string;
@ -40,6 +34,6 @@ export type FilterQdata = {
[key: string]: string | number | boolean | { id: string | number; name?: string }[]; [key: string]: string | number | boolean | { id: string | number; name?: string }[];
}; };
export function isFilterValueless(inputType: InputType): boolean { export function isFilterValueless(inputType: 0 | 1 | 2 | 3): boolean {
return inputType === InputType.None || inputType === InputType.Boolean; return inputType === 0 || inputType === 3;
} }

View file

@ -7,7 +7,6 @@
export let category: Category; export let category: Category;
export let baseUrl = URL_ENTRIES; export let baseUrl = URL_ENTRIES;
export let href: string | undefined = undefined; export let href: string | undefined = undefined;
export let nolink = false;
$: textColor = category.color $: textColor = category.color
? colorToHex(getTextColor(hexToColor(category.color))) ? colorToHex(getTextColor(hexToColor(category.color)))
@ -26,7 +25,7 @@
} }
</script> </script>
{#if href || nolink} {#if href}
<a <a
style:color={category.color ? textColor : undefined} style:color={category.color ? textColor : undefined}
style:background-color={category.color} style:background-color={category.color}

View file

@ -20,7 +20,6 @@
export let entries: RouterOutput["entry"]["list"]; export let entries: RouterOutput["entry"]["list"];
export let baseUrl: string; export let baseUrl: string;
export let patientId: number | null = null; export let patientId: number | null = null;
export let view: string | undefined = undefined;
function paginationUpdate(pagination: PaginationRequest) { function paginationUpdate(pagination: PaginationRequest) {
updateQuery({ updateQuery({
@ -58,7 +57,7 @@
filterData={query.filter} filterData={query.filter}
hiddenFilters={patientId !== null ? ["patient"] : []} hiddenFilters={patientId !== null ? ["patient"] : []}
onUpdate={filterUpdate} onUpdate={filterUpdate}
{view} view="plan"
> >
<slot name="filterbar" /> <slot name="filterbar" />
</FilterBar> </FilterBar>

View file

@ -1,31 +0,0 @@
<script lang="ts">
import type { RouterOutput } from "$lib/shared/trpc";
import { formatDate, formatPatientName } from "$lib/shared/util";
import CategoryField from "$lib/components/table/CategoryField.svelte";
import Markdown from "./markdown/Markdown.svelte";
export let entry: RouterOutput["entry"]["list"]["items"][0];
</script>
<a class="card2 card-animation" href="/entry/{entry.id}">
<div class="row items-center gap-2 text-sm">
<span>{formatDate(entry.current_version.date)}</span>
{#if entry.current_version.category}
<CategoryField category={entry.current_version.category} nolink />
{/if}
{#if entry.current_version.priority}
<div class="badge ellipsis badge-warning">Priorität</div>
{/if}
<span>{formatPatientName(entry.patient)}</span>
<span class="ml-auto hidden md:block">von {entry.current_version.author.name}</span>
</div>
<div class="row">
<div class="line-clamp-3">
<Markdown src={entry.current_version.text} />
</div>
</div>
</a>

View file

@ -7,7 +7,7 @@
export let backHref: string | undefined = undefined; export let backHref: string | undefined = undefined;
</script> </script>
<div class="flex"> <div class="flex flex-row">
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
{#if backHref} {#if backHref}
<a class="btn btn-sm btn-circle btn-ghost" href={backHref}> <a class="btn btn-sm btn-circle btn-ghost" href={backHref}>

View file

@ -1,24 +0,0 @@
<script lang="ts">
import Icon from "./Icon.svelte";
type BtnSize = "sm" | "md";
export let path: string;
export let size: BtnSize = "sm";
export let cls = "";
export let ariaLabel: undefined | string = undefined;
export let ariaPressed: undefined | boolean = undefined;
export let tabindex: undefined | number = undefined;
</script>
<button
class={`btn btn-ghost btn-circle ${
size === "md" ? "" : "btn-" + size
} ${cls}`}
aria-label={ariaLabel}
aria-pressed={ariaPressed}
{tabindex}
on:click
>
<Icon {path} size={size === "md" ? 2 : 1.5} />
</button>

View file

@ -6,8 +6,6 @@
import { PAGINATION_LIMIT } from "$lib/shared/constants"; import { PAGINATION_LIMIT } from "$lib/shared/constants";
import type { Pagination, PaginationRequest } from "$lib/shared/model"; import type { Pagination, PaginationRequest } from "$lib/shared/model";
import { screenWidthSmall } from "$lib/stores/layout";
import Icon from "./Icon.svelte"; import Icon from "./Icon.svelte";
export let paginationData: PaginationRequest | null | undefined; export let paginationData: PaginationRequest | null | undefined;
@ -19,21 +17,18 @@
$: nPages = Math.ceil(data.total / limit); $: nPages = Math.ceil(data.total / limit);
let windowBottom: number; let windowBottom: number;
let windowTop: number; let windowTop: number;
$: if (nPages <= 10) {
$: buttonLim = $screenWidthSmall ? 4 : 10;
$: if (nPages <= buttonLim) {
windowBottom = 1; windowBottom = 1;
windowTop = nPages; windowTop = nPages;
} else if (thisPage <= buttonLim * 0.6) { } else if (thisPage <= 6) {
windowBottom = 1; windowBottom = 1;
windowTop = buttonLim; windowTop = 10;
} else if (thisPage + (buttonLim * 0.4) >= nPages) { } else if (thisPage + 4 >= nPages) {
windowBottom = nPages - buttonLim + 1; windowBottom = nPages - 9;
windowTop = nPages; windowTop = nPages;
} else { } else {
windowBottom = thisPage - Math.floor(buttonLim / 2); windowBottom = thisPage - 5;
windowTop = thisPage + Math.ceil(buttonLim / 2) - 1; windowTop = thisPage + 4;
} }
function getPaginationRequest(page: number): PaginationRequest | null { function getPaginationRequest(page: number): PaginationRequest | null {

View file

@ -1,64 +0,0 @@
<script lang="ts">
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
import { DateRange } from "$lib/shared/util";
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
import { weekFilterItems } from "$lib/components/filter/filters";
import type { BaseItem } from "$lib/components/filter/types";
import IconButton from "./IconButton.svelte";
let editing = false;
let autocomplete: Autocomplete<BaseItem> | undefined;
export let dateRange: DateRange = DateRange.thisWeek();
export let onSelect: (value: DateRange) => void = () => {};
function nextWeek() {
dateRange.addDays(7);
dateRange = dateRange; // update reactive
onSelect(dateRange);
}
function previousWeek() {
dateRange.addDays(-7);
dateRange = dateRange; // update reactive
onSelect(dateRange);
}
function startEditing() {
editing = true;
}
function stopEditing() {
editing = false;
}
$: if (editing && autocomplete) {
autocomplete.open();
}
</script>
<div class="flex items-center">
<IconButton path={mdiChevronLeft} on:click={previousWeek} />
{#if editing}
<Autocomplete
bind:this={autocomplete}
items={async () => weekFilterItems(false)}
onClose={stopEditing}
onSelect={(item) => {
if (typeof item.id === "string") {
const parsed = DateRange.parse(item.id);
if (parsed) dateRange = parsed;
onSelect(dateRange);
}
return { close: true, newValue: "" };
}}
partOfFilterbar
selection={{ id: dateRange.toString(), name: dateRange.format() }} />
{:else}
<button on:click={startEditing}>{dateRange.format()}</button>
{/if}
<IconButton path={mdiChevronRight} on:click={nextWeek} />
</div>

View file

@ -68,7 +68,7 @@
.carta-theme__default .carta-icon-full { .carta-theme__default .carta-icon-full {
border: 0; border: 0;
background: transparent; background: transparent;
@apply btn-animation; @apply btn-transition;
} }
.carta-theme__default .carta-icon-full { .carta-theme__default .carta-icon-full {

View file

@ -10,7 +10,7 @@ import type {
PaginationRequest, PaginationRequest,
SortRequest, SortRequest,
} from "$lib/shared/model"; } from "$lib/shared/model";
import { DateRange, dateToYMD } from "$lib/shared/util"; import { dateToYMD } from "$lib/shared/util";
import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error"; import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error";
import { prisma } from "$lib/server/prisma"; import { prisma } from "$lib/server/prisma";
@ -274,18 +274,6 @@ left join stations s on s.id = r.station_id`,
); );
} }
if (filter?.date) {
filterListToArray(filter.date).forEach((itm) => {
const dateRange = DateRange.parse(itm);
if (dateRange?.start) {
qb.addFilterClause(`ev.date >= ${qb.pvar()}`, dateRange.start);
}
if (dateRange?.end) {
qb.addFilterClause(`ev.date <= ${qb.pvar()}`, dateRange.end);
}
});
}
const SORT_FIELDS: { [key: string]: string[] } = { const SORT_FIELDS: { [key: string]: string[] } = {
id: ["e.id"], id: ["e.id"],
patient: ["p.last_name", "p.first_name"], patient: ["p.last_name", "p.first_name"],
@ -390,23 +378,3 @@ left join stations s on s.id = r.station_id`,
return { items, offset: qb.getOffset(), total }; return { items, offset: qb.getOffset(), total };
} }
/** Get the amount of entries with todo dates before/equal the given date */
export async function getNTodo(date: Date): Promise<number> {
const result = await prisma.$queryRaw`select count(*) from entries e
join entry_versions ev on ev.entry_id = e.id
and ev.id = (
select
id
from
entry_versions ev2
where
ev2.entry_id = ev.entry_id
order by
ev2.created_at desc
limit 1)
where ev.date <= ${date}`;
// @ts-expect-error type checked
const count = Number(result[0].count);
return count;
}

View file

@ -152,7 +152,6 @@ export class QueryBuilder {
return `$${this.nP}`; return `$${this.nP}`;
} }
/** Add a simple filter checking for equality */
addFilter(fname: string, val: unknown | undefined) { addFilter(fname: string, val: unknown | undefined) {
if (val === undefined) return; if (val === undefined) return;
@ -160,13 +159,11 @@ export class QueryBuilder {
this.filterClauses.push(`${fname} = ${this.pvar()}`); this.filterClauses.push(`${fname} = ${this.pvar()}`);
} }
/** Add a SQL filter clause */
addFilterClause(clause: string, ...params: unknown[]) { addFilterClause(clause: string, ...params: unknown[]) {
this.filterClauses.push(clause); this.filterClauses.push(clause);
this.params.push(...params); this.params.push(...params);
} }
/** Add a list filter (value matches any item from the filter list) */
addFilterList(fname: string, fl: FilterList<unknown> | undefined) { addFilterList(fname: string, fl: FilterList<unknown> | undefined) {
if (fl === undefined) return; if (fl === undefined) return;

View file

@ -6,7 +6,6 @@ import {
ZEntryNew, ZEntryNew,
ZEntryVersionNew, ZEntryVersionNew,
ZEntityId, ZEntityId,
fields,
} from "$lib/shared/model/validation"; } from "$lib/shared/model/validation";
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff"; import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
@ -17,7 +16,6 @@ import {
getEntryNExecutions, getEntryNExecutions,
getEntryNVersions, getEntryNVersions,
getEntryVersions, getEntryVersions,
getNTodo,
newEntry, newEntry,
newEntryExecution, newEntryExecution,
newEntryVersion, newEntryVersion,
@ -55,9 +53,6 @@ export const entryRouter = t.router({
const executions = await getEntryExecutions(opts.input); const executions = await getEntryExecutions(opts.input);
return executionsDiff(executions); return executionsDiff(executions);
})), })),
nTodo: t.procedure.input(fields.DateString().optional()).query(async (opts) => trpcWrap(
async () => getNTodo(opts.input ? new Date(opts.input) : new Date()),
)),
create: t.procedure create: t.procedure
.input(ZEntryNew) .input(ZEntryNew)
.mutation(async (opts) => trpcWrap(async () => newEntry(opts.ctx.user.id, opts.input))), .mutation(async (opts) => trpcWrap(async () => newEntry(opts.ctx.user.id, opts.input))),

View file

@ -1,6 +1,4 @@
export const PAGINATION_LIMIT = 20; export const PAGINATION_LIMIT = 20;
export const WEEK_LIMIT = 8;
export const URL_ENTRIES = "/plan"; export const URL_ENTRIES = "/plan";
export const URL_VISIT = "/visit";
export const URL_PATIENTS = "/patients"; export const URL_PATIENTS = "/patients";

View file

@ -26,7 +26,6 @@ export type EntriesFilter = Partial<{
station: FilterList<number>; station: FilterList<number>;
room: FilterList<number>; room: FilterList<number>;
priority: boolean; priority: boolean;
date: FilterList<string>;
}>; }>;
export type PatientsFilter = Partial<{ export type PatientsFilter = Partial<{

View file

@ -1,6 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { DateRange } from "$lib/shared/util";
import { implement } from "$lib/shared/util/zod"; import { implement } from "$lib/shared/util/zod";
import type { import type {
@ -37,7 +36,6 @@ export const fields = {
}, },
{ message: "Datum muss in der Zukunft liegen" }, { message: "Datum muss in der Zukunft liegen" },
), ),
DateRange: () => z.string().refine((val) => Boolean(DateRange.parse(val))),
}; };
const coercedUint = z.coerce.number().int().nonnegative(); const coercedUint = z.coerce.number().int().nonnegative();
@ -140,8 +138,6 @@ const paginatedQuery = <T extends z.ZodTypeAny>(f: T) => z
}) })
.partial(); .partial();
const DateFilterList = z.array(z.object({ id: fields.DateRange(), name: z.string().optional() }));
export const ZEntriesFilter = returnDataInSameOrderAsPassed(z export const ZEntriesFilter = returnDataInSameOrderAsPassed(z
.object({ .object({
author: ZFilterList, author: ZFilterList,
@ -153,7 +149,6 @@ export const ZEntriesFilter = returnDataInSameOrderAsPassed(z
room: ZFilterList, room: ZFilterList,
search: z.string(), search: z.string(),
station: ZFilterList, station: ZFilterList,
date: DateFilterList,
}) })
.partial()); .partial());

View file

@ -3,9 +3,7 @@ import { expect, it, vi } from "vitest";
import type { EntityQuery } from "$lib/shared/model"; import type { EntityQuery } from "$lib/shared/model";
import { ZEntriesQuery } from "$lib/shared/model/validation"; import { ZEntriesQuery } from "$lib/shared/model/validation";
import { import { getQueryUrl, humanDate, parseQueryUrl } from ".";
getQueryUrl, humanDate, DateRange, parseQueryUrl,
} from ".";
const MINUTE = 60000; const MINUTE = 60000;
const HOUR = 3_600_000; const HOUR = 3_600_000;
@ -55,20 +53,3 @@ it("getQueryUrl", () => {
const decoded = ZEntriesQuery.parse(parseQueryUrl(queryUrl)); const decoded = ZEntriesQuery.parse(parseQueryUrl(queryUrl));
expect(decoded).toStrictEqual(query); expect(decoded).toStrictEqual(query);
}); });
it.each([
{ s: "", exp: null },
{ s: "..", exp: null },
{ s: "foo..bar", exp: null },
{ s: "2024-04-15", exp: new DateRange(new Date("2024-04-15"), new Date("2024-04-15")) },
{ s: "2024-04-13..2024-04-20", exp: new DateRange(new Date("2024-04-13"), new Date("2024-04-20")) },
{ s: "2024-04-13..", exp: new DateRange(new Date("2024-04-13"), null) },
{ s: "..2024-04-20", exp: new DateRange(null, new Date("2024-04-20")) },
])("parse date range $s", ({ s, exp }) => {
const res = DateRange.parse(s);
expect(res).toStrictEqual(exp);
if (res && s !== "2024-04-15") {
expect(res.toString()).toBe(s);
}
});

View file

@ -2,12 +2,11 @@ import { goto } from "$app/navigation";
import { isRedirect, error } from "@sveltejs/kit"; import { isRedirect, error } from "@sveltejs/kit";
import { TRPCClientError } from "@trpc/client"; import { TRPCClientError } from "@trpc/client";
import DOMPurify from "isomorphic-dompurify";
import qs from "qs"; import qs from "qs";
import { ZodError } from "zod"; import { ZodError } from "zod";
import DOMPurify from "isomorphic-dompurify";
import type { EntityQuery, Patient } from "$lib/shared/model"; import type { EntityQuery } from "$lib/shared/model";
import type { RouterOutput } from "../trpc";
const LOCALE = "de-DE"; const LOCALE = "de-DE";
@ -162,90 +161,3 @@ export class Debouncer {
export function sanitizeHtml(s: string): string { export function sanitizeHtml(s: string): string {
return DOMPurify.sanitize(s, { FORBID_TAGS: ["img"] }); return DOMPurify.sanitize(s, { FORBID_TAGS: ["img"] });
} }
export class DateRange {
start: Date | null;
end: Date | null;
constructor(start: Date | null, end: Date | null) {
if (start === null && end == null) throw Error("this is not a range");
this.start = start;
this.end = end;
}
/** Construct a new DateRange with the given length in days */
static withLength(start: Date, nDays: number): DateRange {
const end = new Date(start);
end.setDate(start.getDate() + nDays);
return new DateRange(start, end);
}
/** 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;
dayStart.setDate(dayStart.getDate() - daysMinus);
return DateRange.withLength(dayStart, 6);
}
/** Parse a date range from a string
*
* Accepted formats:
* - Single date `2024-04-15` => `2024-04-15..2024-04-15`
* - 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 {
const parts = s.split("..", 2);
const parsed = parts.map((p) => {
if (p.length === 0) return null;
return new Date(p);
});
if (parsed.length === 0
// @ts-expect-error Parsed dates can be NaN
|| parsed.findIndex(isNaN) !== -1
|| parsed.every((e) => e === null)
) return null;
if (parsed.length === 2) {
return new DateRange(parsed[0], parsed[1]);
} else {
return new DateRange(parsed[0], parsed[0]);
}
}
/** Shift the range by the given number of days. This modifies the range in-place */
addDays(n: number) {
this.start?.setDate(this.start.getDate() + n);
this.end?.setDate(this.end.getDate() + n);
}
/** Return a parsable string representation */
toString(): string {
let res = "";
if (this.start) res += dateToYMD(this.start);
res += "..";
if (this.end) res += dateToYMD(this.end);
return res;
}
/** 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;
}
}
export function formatPatientName(patient: RouterOutput["patient"]["list"]["items"][0]): string {
return `${patient.first_name} ${patient.last_name} (${patient.age})`;
}

View file

@ -1,5 +0,0 @@
import { derived, writable } from "svelte/store";
// Width of the main section of the layout
export const screenWidth = writable(0);
export const screenWidthSmall = derived(screenWidth, ($mainWidth) => $mainWidth < 500);

View file

@ -1,8 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/stores"; import { page } from "$app/stores";
import type { PageData } from "./$types";
export let data: PageData;
</script> </script>
<svelte:head> <svelte:head>
@ -31,12 +28,12 @@
<div class="card-body"> <div class="card-body">
<h2 class="card-title">Visite</h2> <h2 class="card-title">Visite</h2>
<p>Hier können sie Visitenbucheinträge abarbeiten.</p> <p>Hier können sie Visitenbucheinträge abarbeiten.</p>
<p>Heute müssen {data.nTodo} Einträge erledigt werden.</p> <p>Heute müssen (n) Einträge erledigt werden.</p>
<div class="card-actions justify-end"> <div class="card-actions justify-end">
<a class="btn btn-primary" href="/visit">Visite</a> <a class="btn btn-primary" href="/visit">Visite</a>
</div> </div>
</div> </div>
</div> </div>
<!-- <a href="/plan/%7B%22filter%22:%7B%22done%22:%22foo%22%7D%7D">Throw error</a> --> <a href="/plan/%7B%22filter%22:%7B%22done%22:%22foo%22%7D%7D">Throw error</a>
</div> </div>

View file

@ -1,12 +0,0 @@
import type { PageLoad } from "./$types";
import { trpc } from "$lib/shared/trpc";
import { loadWrap } from "$lib/shared/util";
export const load: PageLoad = async (event) => {
return loadWrap(async () => {
const nTodo = await trpc(event).entry.nTodo.query();
return { nTodo };
});
};

View file

@ -33,7 +33,7 @@
<Header backHref={basePath} title="Eintrag #{data.entry.id} bearbeiten" /> <Header backHref={basePath} title="Eintrag #{data.entry.id} bearbeiten" />
<p class="text-sm flex gap-2"> <p class="text-sm flex flex-row gap-2">
<span>Erstellt {humanDate(data.entry.created_at, true)}</span> <span>Erstellt {humanDate(data.entry.created_at, true)}</span>
<span>·</span> <span>·</span>
<span> <span>

View file

@ -42,7 +42,6 @@
<FormField label="Patient"> <FormField label="Patient">
<Autocomplete <Autocomplete
clearBtn
filterFn={(itm) => $form.room_id === null || itm.room_id === $form.room_id} filterFn={(itm) => $form.room_id === null || itm.room_id === $form.room_id}
idInputName="patient_id" idInputName="patient_id"
inputCls="input input-bordered w-full max-w-xs" inputCls="input input-bordered w-full max-w-xs"

View file

@ -12,6 +12,6 @@
<title>Planung</title> <title>Planung</title>
</svelte:head> </svelte:head>
<FilteredEntryTable baseUrl={URL_ENTRIES} entries={data.entries} query={data.query} view="plan"> <FilteredEntryTable baseUrl={URL_ENTRIES} entries={data.entries} query={data.query}>
<a slot="filterbar" class="btn btn-sm btn-primary" href="/entry/new">Neuer Eintrag</a> <a slot="filterbar" class="btn btn-sm btn-primary" href="/entry/new">Neuer Eintrag</a>
</FilteredEntryTable> </FilteredEntryTable>

View file

@ -1,70 +1,4 @@
<script lang="ts"> <script lang="ts">
import { browser } from "$app/environment";
import { goto } from "$app/navigation";
import type { PageData } from "./$types";
import { URL_VISIT } from "$lib/shared/constants";
import type { PaginationRequest } 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";
import EntryCard from "$lib/components/ui/EntryCard.svelte";
import PaginationButtons from "$lib/components/ui/PaginationButtons.svelte";
import WeekSelector from "$lib/components/ui/WeekSelector.svelte";
export let data: PageData;
let dateRange: DateRange;
let selection: RouterOutput["station"]["get"] | null;
$: if (data.query.filter?.date) {
const date = data.query.filter?.date[0];
if (date) {
dateRange = DateRange.parse(date.id) ?? DateRange.thisWeek();
} else {
dateRange = DateRange.thisWeek();
}
} else {
dateRange = DateRange.thisWeek();
}
$: if (data.query.filter?.station) {
const station = data.query.filter?.station[0];
if (station) {
selection = { id: station.id, name: station.name ?? "" };
} else {
selection = null;
}
} else {
selection = null;
}
function paginationUpdate(pagination: PaginationRequest) {
updateQuery({
filter: data.query.filter,
pagination,
});
}
function filterUpdate() {
updateQuery({
filter: {
done: false,
station: selection ? [selection] : undefined,
date: dateRange ? [{ id: dateRange.toString(), name: dateRange.format() }] : undefined,
},
});
}
function updateQuery(q: typeof data.query) {
if (browser) {
// Update page URL
const url = getQueryUrl(q, URL_VISIT);
goto(url, { replaceState: true, keepFocus: true });
}
}
</script> </script>
<svelte:head> <svelte:head>
@ -72,33 +6,3 @@
</svelte:head> </svelte:head>
<h1 class="heading">Visite</h1> <h1 class="heading">Visite</h1>
<div class="flex flex-wrap gap-2 justify-between">
<div class="flex gap-2 items-center">
<span>Woche:</span>
<WeekSelector onSelect={filterUpdate} bind:dateRange />
</div>
<div class="flex gap-2 items-center">
<span>Station:</span>
<Autocomplete
clearBtn
inputCls="input input-sm input-bordered"
items={async () => trpc().station.list.query()}
onSelect={filterUpdate}
onUnselect={filterUpdate}
bind:selection />
</div>
</div>
<div class="flex flex-col gap-4">
{#each data.entries.items as entry}
<EntryCard {entry} />
{/each}
</div>
<PaginationButtons
data={data.entries}
onUpdate={paginationUpdate}
paginationData={data.query.pagination}
/>

View file

@ -1,40 +0,0 @@
import type { PageLoad } from "./$types";
import { z } from "zod";
import { ZEntriesQuery } from "$lib/shared/model/validation";
import { trpc } from "$lib/shared/trpc";
import { DateRange, loadWrap, parseQueryUrl } from "$lib/shared/util";
export const load: PageLoad = async (event) => {
return loadWrap(async () => {
let query: z.infer<typeof ZEntriesQuery> = {};
if (event.url.search) {
const decoded = parseQueryUrl(event.url.search);
query = ZEntriesQuery.parse(decoded);
}
// Default filter (current week)
const drange = DateRange.thisWeek();
if (!query.filter) {
query.filter = {
date: [{ id: drange.toString() }],
};
}
if (!query.sort) {
query.sort = { field: "date" };
}
const entries = await trpc(event).entry.list.query(query);
// Move prioritized items to the front
entries.items.forEach((itm, i) => {
if (itm.current_version.priority) {
entries.items.unshift(entries.items.splice(i, 1)[0]);
}
});
return { query, entries };
});
};

View file

@ -4,7 +4,6 @@
import { navigating } from "$app/stores"; import { navigating } from "$app/stores";
import LoadingBar from "$lib/components/ui/LoadingBar.svelte"; import LoadingBar from "$lib/components/ui/LoadingBar.svelte";
import { screenWidth } from "$lib/stores/layout";
let loadingBar: LoadingBar | undefined; let loadingBar: LoadingBar | undefined;
@ -17,6 +16,6 @@
<LoadingBar bind:this={loadingBar} cls="fixed top-0 left-0 z-50" /> <LoadingBar bind:this={loadingBar} cls="fixed top-0 left-0 z-50" />
<div class="bg-base-100 text-base-content" bind:clientWidth={$screenWidth}> <div class="bg-base-100 text-base-content">
<slot /> <slot />
</div> </div>

View file

@ -8,7 +8,6 @@ import {
getEntryNExecutions, getEntryNExecutions,
getEntryNVersions, getEntryNVersions,
getEntryVersions, getEntryVersions,
getNTodo,
newEntry, newEntry,
newEntryExecution, newEntryExecution,
newEntryVersion, newEntryVersion,
@ -270,25 +269,9 @@ test("get entries", async () => {
expect(entriesPrio.total).toBe(1); expect(entriesPrio.total).toBe(1);
expect(entriesPrio.items[0].id).toBe(eId1); expect(entriesPrio.items[0].id).toBe(eId1);
// Filter by date range
const entriesDate = await getEntries({ date: "2024-01-05" });
expect(entriesDate.items).length(1);
expect(entriesDate.total).toBe(1);
expect(entriesDate.items[0].id).toBe(eId2);
const entriesDateRange = await getEntries({ date: "2024-01-05..2024-01-06" });
expect(entriesDateRange.items).length(2);
expect(entriesDateRange.total).toBe(2);
expect(entriesDateRange.items[0].id).toBe(eId3);
expect(entriesDateRange.items[1].id).toBe(eId2);
// Search // Search
const entriesSearch = await getEntries({ search: "Blu" }, {}); const entriesSearch = await getEntries({ search: "Blu" }, {});
expect(entriesSearch.items).length(1); expect(entriesSearch.items).length(1);
expect(entriesSearch.total).toBe(1); expect(entriesSearch.total).toBe(1);
expect(entriesSearch.items[0].id).toBe(eId1); expect(entriesSearch.items[0].id).toBe(eId1);
// NTodo
const n = await getNTodo(new Date("2024-01-05"));
expect(n).toBe(2);
}); });