Compare commits
No commits in common. "cfe5fc2d9f012aafc8aa470a5b2f8d1366d29dcc" and "152824f6c071b4f647be90d80c680cbd8ce736f3" have entirely different histories.
cfe5fc2d9f
...
152824f6c0
43 changed files with 110 additions and 649 deletions
|
@ -1,10 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
36
Dockerfile
36
Dockerfile
|
@ -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"]
|
|
@ -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
|
||||
// also, prefer `a || b` over `a ? a : b`
|
||||
// https://eslint.org/docs/rules/no-unneeded-ternary
|
||||
|
|
|
@ -21,10 +21,9 @@
|
|||
"@floating-ui/core": "^1.6.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@prisma/client": "^5.12.1",
|
||||
"carta-md": "4.0.2",
|
||||
"carta-md": "^4.0.0",
|
||||
"diff": "^5.2.0",
|
||||
"isomorphic-dompurify": "^2.7.0",
|
||||
"prisma": "^5.12.1",
|
||||
"qs": "^6.12.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
|
@ -57,6 +56,7 @@
|
|||
"globals": "^15.0.0",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-nesting": "^12.1.1",
|
||||
"prisma": "^5.12.1",
|
||||
"svelte": "^4.2.15",
|
||||
"svelte-check": "^3.6.9",
|
||||
"sveltekit-superforms": "^2.12.5",
|
||||
|
@ -72,7 +72,7 @@
|
|||
"type": "module",
|
||||
"pnpm": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ settings:
|
|||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
carta-md@4.0.2:
|
||||
hash: i33ea43vfgrg3ziu25cfu7s2zq
|
||||
path: patches/carta-md@4.0.2.patch
|
||||
carta-md@4.0.0:
|
||||
hash: 4dz4dhp4dewr3xkzk2rwum2jdq
|
||||
path: patches/carta-md@4.0.0.patch
|
||||
|
||||
dependencies:
|
||||
'@auth/core':
|
||||
|
@ -23,8 +23,8 @@ dependencies:
|
|||
specifier: ^5.12.1
|
||||
version: 5.12.1(prisma@5.12.1)
|
||||
carta-md:
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2(patch_hash=i33ea43vfgrg3ziu25cfu7s2zq)(svelte@4.2.15)
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(patch_hash=4dz4dhp4dewr3xkzk2rwum2jdq)(svelte@4.2.15)
|
||||
diff:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
|
@ -1075,8 +1075,8 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/@shikijs/core@1.4.0:
|
||||
resolution: {integrity: sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==}
|
||||
/@shikijs/core@1.3.0:
|
||||
resolution: {integrity: sha512-7fedsBfuILDTBmrYZNFI8B6ATTxhQAasUHllHmjvSZPnoq4bULWoTpHwmuQvZ8Aq03/tAa2IGo6RXqWtHdWaCA==}
|
||||
dev: false
|
||||
|
||||
/@sideway/address@4.1.5:
|
||||
|
@ -1935,8 +1935,8 @@ packages:
|
|||
resolution: {integrity: sha512-19NuN1/3PjA3QI8Eki55N8my4LzfkMCRLgCVfrl/slbSAchQfV0+GwjPrK3rq37As4UCLlM/DHajbKkAqbv92Q==}
|
||||
dev: true
|
||||
|
||||
/carta-md@4.0.2(patch_hash=i33ea43vfgrg3ziu25cfu7s2zq)(svelte@4.2.15):
|
||||
resolution: {integrity: sha512-wMlw0r5RZiVwvF3dyxE/vHj9pXlXbzpijJ3m/o9zqZe7Cf6D96AjyBHBpa0A0OPj/uEJVF3k0R6ctopBJCpCQg==}
|
||||
/carta-md@4.0.0(patch_hash=4dz4dhp4dewr3xkzk2rwum2jdq)(svelte@4.2.15):
|
||||
resolution: {integrity: sha512-mxIoN3dqcjgv8i5FIUBH69lclx8A1/FB/FaFymWBzKE4AvUdy/X6VQGBNzAO3ybSAdceMT0RrAhY5/KnoFI8Hg==}
|
||||
peerDependencies:
|
||||
svelte: ^3.54.0 || ^4.0.0
|
||||
dependencies:
|
||||
|
@ -1944,7 +1944,7 @@ packages:
|
|||
remark-gfm: 4.0.0
|
||||
remark-parse: 11.0.0
|
||||
remark-rehype: 11.1.0
|
||||
shiki: 1.4.0
|
||||
shiki: 1.3.0
|
||||
svelte: 4.2.15
|
||||
unified: 11.0.4
|
||||
transitivePeerDependencies:
|
||||
|
@ -4860,10 +4860,10 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/shiki@1.4.0:
|
||||
resolution: {integrity: sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==}
|
||||
/shiki@1.3.0:
|
||||
resolution: {integrity: sha512-9aNdQy/etMXctnPzsje1h1XIGm9YfRcSksKOGqZWXA/qP9G18/8fpz5Bjpma8bOgz3tqIpjERAd6/lLjFyzoww==}
|
||||
dependencies:
|
||||
'@shikijs/core': 1.4.0
|
||||
'@shikijs/core': 1.3.0
|
||||
dev: false
|
||||
|
||||
/side-channel@1.0.6:
|
||||
|
|
15
src/app.pcss
15
src/app.pcss
|
@ -4,9 +4,11 @@
|
|||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.btn-animation {
|
||||
.btn-transition {
|
||||
@apply duration-200 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:focus {
|
||||
animation: button-pop 0s ease-out;
|
||||
|
@ -45,7 +47,7 @@ button {
|
|||
@apply border border-solid border-base-content/30;
|
||||
|
||||
.row {
|
||||
@apply flex;
|
||||
@apply flex flex-row;
|
||||
}
|
||||
|
||||
.row,
|
||||
|
@ -74,12 +76,3 @@ button {
|
|||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,3 @@ export const handle = sequence(
|
|||
authorization,
|
||||
createTRPCHandle({ router, createContext }),
|
||||
);
|
||||
|
||||
// Allow server application to exit
|
||||
process.on("SIGINT", process.exit); // Ctrl+C
|
||||
process.on("SIGTERM", process.exit); // docker stop
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</a>
|
||||
</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>·</span>
|
||||
{#if entry.execution}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
import { formatPatientName } from "$lib/shared/util";
|
||||
|
||||
import RoomField from "$lib/components/table/RoomField.svelte";
|
||||
|
||||
|
@ -14,7 +13,9 @@
|
|||
<RoomField room={patient.room} />
|
||||
{/if}
|
||||
<a href="/patient/{patient.id}">
|
||||
{formatPatientName(patient)}
|
||||
{patient.first_name}
|
||||
{patient.last_name}
|
||||
({patient.age})
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,17 +1,4 @@
|
|||
<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
|
||||
* Original source: https://github.com/pstanoev/simple-svelte-autocomplete
|
||||
|
@ -19,6 +6,17 @@
|
|||
* 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 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
|
||||
* (to allow Svelte Superforms to work) */
|
||||
export let idInputName: string | undefined = undefined;
|
||||
export let clearBtn = false;
|
||||
/** Function to filter shown items */
|
||||
export let filterFn: (item: T) => boolean = () => true;
|
||||
|
||||
|
@ -281,7 +278,7 @@
|
|||
}
|
||||
</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
|
||||
bind:this={inputElm}
|
||||
class={inputCls}
|
||||
|
@ -332,12 +329,6 @@
|
|||
<!-- Hidden input field (acting as form input) -->
|
||||
<input name={idInputName} type="hidden" value={selection?.id ?? ""} />
|
||||
{/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>
|
||||
|
||||
<style lang="postcss">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
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 EntryFilterChip from "./FilterChip.svelte";
|
||||
|
@ -16,7 +16,7 @@
|
|||
BaseItem,
|
||||
SelectionOrText,
|
||||
} from "./types";
|
||||
import { InputType, isFilterValueless } from "./types";
|
||||
import { isFilterValueless } from "./types";
|
||||
|
||||
/** Filter definitions */
|
||||
export let FILTERS: { [key: string]: FilterDef };
|
||||
|
@ -28,7 +28,7 @@
|
|||
export let hiddenFilters: string[] = [];
|
||||
/** True if a separate search field should be displayed */
|
||||
export let search = false;
|
||||
export let view: string | undefined = undefined;
|
||||
export let view: string;
|
||||
|
||||
let autocomplete: Autocomplete<BaseItem> | undefined;
|
||||
let activeFilters: FilterData[] = [];
|
||||
|
@ -116,24 +116,26 @@
|
|||
const key = filter.id;
|
||||
|
||||
let val = null;
|
||||
if (filter.inputType === InputType.None) {
|
||||
if (filter.inputType === 0) {
|
||||
// Valueless filter (val = true)
|
||||
val = true;
|
||||
} else if (filter.inputType === InputType.FreeText) {
|
||||
} else if (filter.inputType === 1) {
|
||||
// Text input
|
||||
val = fdata.selection?.name;
|
||||
} else if (filter.inputType === InputType.FilterList && fdata.selection) {
|
||||
} else if (filter.inputType === 2 && fdata.selection) {
|
||||
// Filter list
|
||||
val = [{ id: fdata.selection.id!, name: fdata.selection.name }];
|
||||
} else if (filter.inputType === InputType.Boolean) {
|
||||
// @ts-expect-error TODO
|
||||
val = { id: fdata.selection.id, name: fdata.selection.name };
|
||||
} else if (filter.inputType === 3) {
|
||||
val = Boolean(fdata.selection?.toggle);
|
||||
}
|
||||
|
||||
if (val !== null && val !== undefined) {
|
||||
if (filter.inputType === 2) {
|
||||
// @ts-expect-error fd[key] is checked
|
||||
if (Array.isArray(fd[key])) fd[key].push(...val);
|
||||
else fd[key] = val;
|
||||
if (Array.isArray(fd[key])) fd[key].push(val);
|
||||
// @ts-expect-error fd[key] is checked
|
||||
else fd[key] = [val];
|
||||
} else {
|
||||
fd[key] = val;
|
||||
}
|
||||
|
@ -221,15 +223,17 @@
|
|||
partOfFilterbar
|
||||
placeholder="Filter"
|
||||
/>
|
||||
<IconButton
|
||||
ariaLabel="Alle Filter entfernen"
|
||||
cls="absolute bottom-0 right-0"
|
||||
path={mdiClose}
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-ghost absolute bottom-0 right-0"
|
||||
aria-label="Alle Filter entfernen"
|
||||
on:click={() => {
|
||||
activeFilters = [];
|
||||
searchVal = "";
|
||||
updateFilter();
|
||||
}} />
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiClose} size={1.2} />
|
||||
</button>
|
||||
</div>
|
||||
{#if search}
|
||||
<input
|
||||
|
@ -244,9 +248,7 @@
|
|||
<slot />
|
||||
</div>
|
||||
|
||||
{#if view}
|
||||
<SavedFilters {view} />
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.filterbar-outer {
|
||||
|
|
|
@ -4,9 +4,8 @@
|
|||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
|
||||
import Autocomplete from "./Autocomplete.svelte";
|
||||
import {
|
||||
InputType,
|
||||
type BaseItem, type FilterData, type FilterDef, type SelectionOrText,
|
||||
import type {
|
||||
BaseItem, FilterData, FilterDef, SelectionOrText,
|
||||
} from "./types";
|
||||
|
||||
export let filter: FilterDef;
|
||||
|
@ -37,7 +36,7 @@
|
|||
autocomplete.open();
|
||||
}
|
||||
|
||||
const TOFF = " aus"; // Toggle name suffix (in case toggleOff.name is unset)
|
||||
const TOFF = " aus";
|
||||
|
||||
$: toggleState = fdata.selection?.toggle !== false;
|
||||
$: filterName = toggleState
|
||||
|
@ -73,7 +72,7 @@
|
|||
|
||||
{#if hasInputField}
|
||||
{#if fdata.editing}
|
||||
{#if filter.inputType === InputType.FilterList}
|
||||
{#if filter.inputType === 2}
|
||||
{@const hids = hiddenIds()}
|
||||
<Autocomplete
|
||||
bind:this={autocomplete}
|
||||
|
@ -84,8 +83,8 @@
|
|||
onBackspace={onRemove}
|
||||
{onClose}
|
||||
onSelect={(item) => {
|
||||
// Accept the selection if the user selected a variant
|
||||
if (item.id) {
|
||||
// Accept the selection if this is a free text field or the user selected a variant
|
||||
if (filter.inputType !== 2 || item.id) {
|
||||
fdata.selection = item;
|
||||
return { close: true, newValue: "" };
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<a class="chip btn-animation" {href}>
|
||||
<a class="chip btn-transition" {href}>
|
||||
<span class="mx-2">
|
||||
<slot />
|
||||
</span>
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
}
|
||||
</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">
|
||||
Gespeicherte Filter:
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
mdiAccountMultipleOutline,
|
||||
mdiAccountRemoveOutline,
|
||||
mdiBedKingOutline,
|
||||
mdiCalendar,
|
||||
mdiCheckboxBlankOutline,
|
||||
mdiCheckboxOutline,
|
||||
mdiDoctor,
|
||||
|
@ -14,75 +13,58 @@ import {
|
|||
mdiTag,
|
||||
} from "@mdi/js";
|
||||
|
||||
import { WEEK_LIMIT } from "$lib/shared/constants";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { DateRange, dateToYMD } from "$lib/shared/util";
|
||||
|
||||
import { type FilterDef, InputType, type BaseItem } 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;
|
||||
}
|
||||
import type { FilterDef } from "./types";
|
||||
|
||||
export const ENTRY_FILTERS: { [key: string]: FilterDef } = {
|
||||
category: {
|
||||
id: "category",
|
||||
name: "Kategorie",
|
||||
icon: mdiTag,
|
||||
inputType: InputType.FilterList,
|
||||
inputType: 2,
|
||||
options: async () => trpc().category.list.query(),
|
||||
},
|
||||
author: {
|
||||
id: "author",
|
||||
name: "Autor",
|
||||
icon: mdiAccount,
|
||||
inputType: InputType.FilterList,
|
||||
inputType: 2,
|
||||
options: async () => trpc().user.getNames.query(),
|
||||
},
|
||||
executor: {
|
||||
id: "executor",
|
||||
name: "Erledigt von",
|
||||
icon: mdiDoctor,
|
||||
inputType: InputType.FilterList,
|
||||
inputType: 2,
|
||||
options: async () => trpc().user.getNames.query(),
|
||||
},
|
||||
patient: {
|
||||
id: "patient",
|
||||
name: "Patient",
|
||||
icon: mdiAccountInjury,
|
||||
inputType: InputType.FilterList,
|
||||
inputType: 2,
|
||||
options: async () => trpc().patient.getNames.query(),
|
||||
},
|
||||
station: {
|
||||
id: "station",
|
||||
name: "Station",
|
||||
icon: mdiDomain,
|
||||
inputType: InputType.FilterList,
|
||||
inputType: 2,
|
||||
options: async () => trpc().station.list.query(),
|
||||
},
|
||||
room: {
|
||||
id: "room",
|
||||
name: "Zimmer",
|
||||
icon: mdiBedKingOutline,
|
||||
inputType: InputType.FilterList,
|
||||
inputType: 2,
|
||||
options: async () => trpc().room.list.query(),
|
||||
},
|
||||
done: {
|
||||
id: "done",
|
||||
name: "Erledigt",
|
||||
icon: mdiCheckboxOutline,
|
||||
inputType: InputType.Boolean,
|
||||
inputType: 3,
|
||||
toggleOff: {
|
||||
name: "Zu erledigen",
|
||||
icon: mdiCheckboxBlankOutline,
|
||||
|
@ -92,20 +74,13 @@ export const ENTRY_FILTERS: { [key: string]: FilterDef } = {
|
|||
id: "priority",
|
||||
name: "Priorität",
|
||||
icon: mdiExclamation,
|
||||
inputType: InputType.None,
|
||||
inputType: 0,
|
||||
},
|
||||
search: {
|
||||
id: "search",
|
||||
name: "Beschreibung",
|
||||
icon: mdiMagnify,
|
||||
inputType: InputType.FreeText,
|
||||
},
|
||||
date: {
|
||||
id: "date",
|
||||
name: "Woche",
|
||||
icon: mdiCalendar,
|
||||
inputType: InputType.FilterList,
|
||||
options: async () => weekFilterItems(true),
|
||||
inputType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,3 @@
|
|||
export enum InputType {
|
||||
None = 0,
|
||||
FreeText = 1,
|
||||
FilterList = 2,
|
||||
Boolean = 3,
|
||||
}
|
||||
|
||||
export type BaseItem = {
|
||||
id: string | number;
|
||||
name: string;
|
||||
|
@ -22,7 +15,8 @@ export type FilterDef = {
|
|||
id: string;
|
||||
name: 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?: {
|
||||
name: string;
|
||||
icon?: string;
|
||||
|
@ -40,6 +34,6 @@ export type FilterQdata = {
|
|||
[key: string]: string | number | boolean | { id: string | number; name?: string }[];
|
||||
};
|
||||
|
||||
export function isFilterValueless(inputType: InputType): boolean {
|
||||
return inputType === InputType.None || inputType === InputType.Boolean;
|
||||
export function isFilterValueless(inputType: 0 | 1 | 2 | 3): boolean {
|
||||
return inputType === 0 || inputType === 3;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
export let category: Category;
|
||||
export let baseUrl = URL_ENTRIES;
|
||||
export let href: string | undefined = undefined;
|
||||
export let nolink = false;
|
||||
|
||||
$: textColor = category.color
|
||||
? colorToHex(getTextColor(hexToColor(category.color)))
|
||||
|
@ -26,7 +25,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#if href || nolink}
|
||||
{#if href}
|
||||
<a
|
||||
style:color={category.color ? textColor : undefined}
|
||||
style:background-color={category.color}
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
export let entries: RouterOutput["entry"]["list"];
|
||||
export let baseUrl: string;
|
||||
export let patientId: number | null = null;
|
||||
export let view: string | undefined = undefined;
|
||||
|
||||
function paginationUpdate(pagination: PaginationRequest) {
|
||||
updateQuery({
|
||||
|
@ -58,7 +57,7 @@
|
|||
filterData={query.filter}
|
||||
hiddenFilters={patientId !== null ? ["patient"] : []}
|
||||
onUpdate={filterUpdate}
|
||||
{view}
|
||||
view="plan"
|
||||
>
|
||||
<slot name="filterbar" />
|
||||
</FilterBar>
|
||||
|
|
|
@ -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>
|
|
@ -7,7 +7,7 @@
|
|||
export let backHref: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{#if backHref}
|
||||
<a class="btn btn-sm btn-circle btn-ghost" href={backHref}>
|
||||
|
|
|
@ -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>
|
|
@ -6,8 +6,6 @@
|
|||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
||||
import type { Pagination, PaginationRequest } from "$lib/shared/model";
|
||||
|
||||
import { screenWidthSmall } from "$lib/stores/layout";
|
||||
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
export let paginationData: PaginationRequest | null | undefined;
|
||||
|
@ -19,21 +17,18 @@
|
|||
$: nPages = Math.ceil(data.total / limit);
|
||||
let windowBottom: number;
|
||||
let windowTop: number;
|
||||
|
||||
$: buttonLim = $screenWidthSmall ? 4 : 10;
|
||||
|
||||
$: if (nPages <= buttonLim) {
|
||||
$: if (nPages <= 10) {
|
||||
windowBottom = 1;
|
||||
windowTop = nPages;
|
||||
} else if (thisPage <= buttonLim * 0.6) {
|
||||
} else if (thisPage <= 6) {
|
||||
windowBottom = 1;
|
||||
windowTop = buttonLim;
|
||||
} else if (thisPage + (buttonLim * 0.4) >= nPages) {
|
||||
windowBottom = nPages - buttonLim + 1;
|
||||
windowTop = 10;
|
||||
} else if (thisPage + 4 >= nPages) {
|
||||
windowBottom = nPages - 9;
|
||||
windowTop = nPages;
|
||||
} else {
|
||||
windowBottom = thisPage - Math.floor(buttonLim / 2);
|
||||
windowTop = thisPage + Math.ceil(buttonLim / 2) - 1;
|
||||
windowBottom = thisPage - 5;
|
||||
windowTop = thisPage + 4;
|
||||
}
|
||||
|
||||
function getPaginationRequest(page: number): PaginationRequest | null {
|
||||
|
|
|
@ -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>
|
|
@ -68,7 +68,7 @@
|
|||
.carta-theme__default .carta-icon-full {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
@apply btn-animation;
|
||||
@apply btn-transition;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-icon-full {
|
||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
|||
PaginationRequest,
|
||||
SortRequest,
|
||||
} 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 { 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[] } = {
|
||||
id: ["e.id"],
|
||||
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 };
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
|
|
@ -152,7 +152,6 @@ export class QueryBuilder {
|
|||
return `$${this.nP}`;
|
||||
}
|
||||
|
||||
/** Add a simple filter checking for equality */
|
||||
addFilter(fname: string, val: unknown | undefined) {
|
||||
if (val === undefined) return;
|
||||
|
||||
|
@ -160,13 +159,11 @@ export class QueryBuilder {
|
|||
this.filterClauses.push(`${fname} = ${this.pvar()}`);
|
||||
}
|
||||
|
||||
/** Add a SQL filter clause */
|
||||
addFilterClause(clause: string, ...params: unknown[]) {
|
||||
this.filterClauses.push(clause);
|
||||
this.params.push(...params);
|
||||
}
|
||||
|
||||
/** Add a list filter (value matches any item from the filter list) */
|
||||
addFilterList(fname: string, fl: FilterList<unknown> | undefined) {
|
||||
if (fl === undefined) return;
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
ZEntryNew,
|
||||
ZEntryVersionNew,
|
||||
ZEntityId,
|
||||
fields,
|
||||
} from "$lib/shared/model/validation";
|
||||
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
|
||||
|
||||
|
@ -17,7 +16,6 @@ import {
|
|||
getEntryNExecutions,
|
||||
getEntryNVersions,
|
||||
getEntryVersions,
|
||||
getNTodo,
|
||||
newEntry,
|
||||
newEntryExecution,
|
||||
newEntryVersion,
|
||||
|
@ -55,9 +53,6 @@ export const entryRouter = t.router({
|
|||
const executions = await getEntryExecutions(opts.input);
|
||||
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
|
||||
.input(ZEntryNew)
|
||||
.mutation(async (opts) => trpcWrap(async () => newEntry(opts.ctx.user.id, opts.input))),
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
export const PAGINATION_LIMIT = 20;
|
||||
export const WEEK_LIMIT = 8;
|
||||
|
||||
export const URL_ENTRIES = "/plan";
|
||||
export const URL_VISIT = "/visit";
|
||||
export const URL_PATIENTS = "/patients";
|
||||
|
|
|
@ -26,7 +26,6 @@ export type EntriesFilter = Partial<{
|
|||
station: FilterList<number>;
|
||||
room: FilterList<number>;
|
||||
priority: boolean;
|
||||
date: FilterList<string>;
|
||||
}>;
|
||||
|
||||
export type PatientsFilter = Partial<{
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { DateRange } from "$lib/shared/util";
|
||||
import { implement } from "$lib/shared/util/zod";
|
||||
|
||||
import type {
|
||||
|
@ -37,7 +36,6 @@ export const fields = {
|
|||
},
|
||||
{ message: "Datum muss in der Zukunft liegen" },
|
||||
),
|
||||
DateRange: () => z.string().refine((val) => Boolean(DateRange.parse(val))),
|
||||
};
|
||||
|
||||
const coercedUint = z.coerce.number().int().nonnegative();
|
||||
|
@ -140,8 +138,6 @@ const paginatedQuery = <T extends z.ZodTypeAny>(f: T) => z
|
|||
})
|
||||
.partial();
|
||||
|
||||
const DateFilterList = z.array(z.object({ id: fields.DateRange(), name: z.string().optional() }));
|
||||
|
||||
export const ZEntriesFilter = returnDataInSameOrderAsPassed(z
|
||||
.object({
|
||||
author: ZFilterList,
|
||||
|
@ -153,7 +149,6 @@ export const ZEntriesFilter = returnDataInSameOrderAsPassed(z
|
|||
room: ZFilterList,
|
||||
search: z.string(),
|
||||
station: ZFilterList,
|
||||
date: DateFilterList,
|
||||
})
|
||||
.partial());
|
||||
|
||||
|
|
|
@ -3,9 +3,7 @@ import { expect, it, vi } from "vitest";
|
|||
import type { EntityQuery } from "$lib/shared/model";
|
||||
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
getQueryUrl, humanDate, DateRange, parseQueryUrl,
|
||||
} from ".";
|
||||
import { getQueryUrl, humanDate, parseQueryUrl } from ".";
|
||||
|
||||
const MINUTE = 60000;
|
||||
const HOUR = 3_600_000;
|
||||
|
@ -55,20 +53,3 @@ it("getQueryUrl", () => {
|
|||
const decoded = ZEntriesQuery.parse(parseQueryUrl(queryUrl));
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,12 +2,11 @@ import { goto } from "$app/navigation";
|
|||
|
||||
import { isRedirect, error } from "@sveltejs/kit";
|
||||
import { TRPCClientError } from "@trpc/client";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import qs from "qs";
|
||||
import { ZodError } from "zod";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
import type { EntityQuery, Patient } from "$lib/shared/model";
|
||||
import type { RouterOutput } from "../trpc";
|
||||
import type { EntityQuery } from "$lib/shared/model";
|
||||
|
||||
const LOCALE = "de-DE";
|
||||
|
||||
|
@ -162,90 +161,3 @@ export class Debouncer {
|
|||
export function sanitizeHtml(s: string): string {
|
||||
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})`;
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -1,8 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -31,12 +28,12 @@
|
|||
<div class="card-body">
|
||||
<h2 class="card-title">Visite</h2>
|
||||
<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">
|
||||
<a class="btn btn-primary" href="/visit">Visite</a>
|
||||
</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>
|
||||
|
|
|
@ -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 };
|
||||
});
|
||||
};
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<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>·</span>
|
||||
<span>
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
|
||||
<FormField label="Patient">
|
||||
<Autocomplete
|
||||
clearBtn
|
||||
filterFn={(itm) => $form.room_id === null || itm.room_id === $form.room_id}
|
||||
idInputName="patient_id"
|
||||
inputCls="input input-bordered w-full max-w-xs"
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
<title>Planung</title>
|
||||
</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>
|
||||
</FilteredEntryTable>
|
||||
|
|
|
@ -1,70 +1,4 @@
|
|||
<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>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -72,33 +6,3 @@
|
|||
</svelte:head>
|
||||
|
||||
<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}
|
||||
/>
|
||||
|
|
|
@ -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 };
|
||||
});
|
||||
};
|
|
@ -4,7 +4,6 @@
|
|||
import { navigating } from "$app/stores";
|
||||
|
||||
import LoadingBar from "$lib/components/ui/LoadingBar.svelte";
|
||||
import { screenWidth } from "$lib/stores/layout";
|
||||
|
||||
let loadingBar: LoadingBar | undefined;
|
||||
|
||||
|
@ -17,6 +16,6 @@
|
|||
|
||||
<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 />
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
getEntryNExecutions,
|
||||
getEntryNVersions,
|
||||
getEntryVersions,
|
||||
getNTodo,
|
||||
newEntry,
|
||||
newEntryExecution,
|
||||
newEntryVersion,
|
||||
|
@ -270,25 +269,9 @@ test("get entries", async () => {
|
|||
expect(entriesPrio.total).toBe(1);
|
||||
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
|
||||
const entriesSearch = await getEntries({ search: "Blu" }, {});
|
||||
expect(entriesSearch.items).length(1);
|
||||
expect(entriesSearch.total).toBe(1);
|
||||
expect(entriesSearch.items[0].id).toBe(eId1);
|
||||
|
||||
// NTodo
|
||||
const n = await getNTodo(new Date("2024-01-05"));
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue