Compare commits
4 commits
b21c1db887
...
622c8fdd39
Author | SHA1 | Date | |
---|---|---|---|
622c8fdd39 | |||
6589af741b | |||
f5239c787c | |||
c7ea0aa8db |
37 changed files with 611 additions and 123 deletions
|
@ -23,6 +23,7 @@
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@prisma/client": "^5.8.1",
|
"@prisma/client": "^5.8.1",
|
||||||
"svelte-floating-ui": "^1.5.8",
|
"svelte-floating-ui": "^1.5.8",
|
||||||
|
"svelte-markdown": "^0.4.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -23,6 +23,9 @@ dependencies:
|
||||||
svelte-floating-ui:
|
svelte-floating-ui:
|
||||||
specifier: ^1.5.8
|
specifier: ^1.5.8
|
||||||
version: 1.5.8
|
version: 1.5.8
|
||||||
|
svelte-markdown:
|
||||||
|
specifier: ^0.4.1
|
||||||
|
version: 0.4.1(svelte@4.2.9)
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.22.4
|
specifier: ^3.22.4
|
||||||
version: 3.22.4
|
version: 3.22.4
|
||||||
|
@ -856,6 +859,10 @@ packages:
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/marked@5.0.2:
|
||||||
|
resolution: {integrity: sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/node@20.11.7:
|
/@types/node@20.11.7:
|
||||||
resolution: {integrity: sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==}
|
resolution: {integrity: sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2096,6 +2103,12 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
|
|
||||||
|
/marked@5.1.2:
|
||||||
|
resolution: {integrity: sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mdn-data@2.0.30:
|
/mdn-data@2.0.30:
|
||||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||||
|
|
||||||
|
@ -2872,6 +2885,16 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
svelte: 4.2.9
|
svelte: 4.2.9
|
||||||
|
|
||||||
|
/svelte-markdown@0.4.1(svelte@4.2.9):
|
||||||
|
resolution: {integrity: sha512-pOlLY6EruKJaWI9my/2bKX8PdTeP5CM0s4VMmwmC2prlOkjAf+AOmTM4wW/l19Y6WZ87YmP8+ZCJCCwBChWjYw==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^4.0.0
|
||||||
|
dependencies:
|
||||||
|
'@types/marked': 5.0.2
|
||||||
|
marked: 5.1.2
|
||||||
|
svelte: 4.2.9
|
||||||
|
dev: false
|
||||||
|
|
||||||
/svelte-preprocess@5.1.3(postcss@8.4.33)(svelte@4.2.9)(typescript@5.3.3):
|
/svelte-preprocess@5.1.3(postcss@8.4.33)(svelte@4.2.9)(typescript@5.3.3):
|
||||||
resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
|
resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
|
||||||
engines: {node: '>= 16.0.0', pnpm: ^8.0.0}
|
engines: {node: '>= 16.0.0', pnpm: ^8.0.0}
|
||||||
|
|
|
@ -78,7 +78,7 @@ CREATE TABLE "entry_versions" (
|
||||||
"id" SERIAL NOT NULL,
|
"id" SERIAL NOT NULL,
|
||||||
"entry_id" INTEGER NOT NULL,
|
"entry_id" INTEGER NOT NULL,
|
||||||
"text" TEXT NOT NULL,
|
"text" TEXT NOT NULL,
|
||||||
"date" TIMESTAMP(3) NOT NULL,
|
"date" DATE NOT NULL,
|
||||||
"category_id" INTEGER,
|
"category_id" INTEGER,
|
||||||
"priority" BOOLEAN NOT NULL,
|
"priority" BOOLEAN NOT NULL,
|
||||||
"author_id" INTEGER NOT NULL,
|
"author_id" INTEGER NOT NULL,
|
||||||
|
|
|
@ -116,7 +116,7 @@ model EntryVersion {
|
||||||
entry_id Int
|
entry_id Int
|
||||||
|
|
||||||
text String
|
text String
|
||||||
date DateTime
|
date DateTime @db.Date
|
||||||
|
|
||||||
category Category? @relation(fields: [category_id], references: [id], onDelete: SetNull)
|
category Category? @relation(fields: [category_id], references: [id], onDelete: SetNull)
|
||||||
category_id Int?
|
category_id Int?
|
||||||
|
|
|
@ -7,6 +7,10 @@ button {
|
||||||
text-align: initial;
|
text-align: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
@apply text-2xl font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
.ellipsis {
|
.ellipsis {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
28
src/hooks.client.ts
Normal file
28
src/hooks.client.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import type { HandleClientError } from "@sveltejs/kit";
|
||||||
|
import { TRPCClientError } from "@trpc/client";
|
||||||
|
|
||||||
|
const CHECK_CONNECTION =
|
||||||
|
"Die Seite konnte nicht geladen werden, prüfen sie ihre Verbindung";
|
||||||
|
|
||||||
|
export const handleError: HandleClientError = async ({ error, message, status }) => {
|
||||||
|
// If there are client-side errors, SvelteKit always returns the nondescript
|
||||||
|
// "Internal error" message. The most common errors should be mapped to a more
|
||||||
|
// detailed description
|
||||||
|
if (status === 500) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Client error:", error);
|
||||||
|
|
||||||
|
if (
|
||||||
|
error instanceof TypeError &&
|
||||||
|
error.message.includes("dynamically imported module")
|
||||||
|
) {
|
||||||
|
// Could not load JS module
|
||||||
|
message = CHECK_CONNECTION;
|
||||||
|
} else if (error instanceof TRPCClientError && !error.data) {
|
||||||
|
// Could not load fetched data
|
||||||
|
message = CHECK_CONNECTION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message };
|
||||||
|
};
|
|
@ -65,6 +65,7 @@ export const handle = sequence(
|
||||||
return opt.session;
|
return opt.session;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
trustHost: true,
|
||||||
}),
|
}),
|
||||||
authorization,
|
authorization,
|
||||||
createTRPCHandle({ router, createContext })
|
createTRPCHandle({ router, createContext })
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
export let cacheKey: string | undefined = undefined;
|
export let cacheKey: string | undefined = undefined;
|
||||||
export let placeholder: string | undefined = undefined;
|
export let placeholder: string | undefined = undefined;
|
||||||
export let padding = true;
|
export let padding = true;
|
||||||
|
export let cls = "";
|
||||||
|
|
||||||
/** Selection callback. Returns the new input value after selection */
|
/** Selection callback. Returns the new input value after selection */
|
||||||
export let onSelect: (item: SelectionOrText, kb: boolean) => OnSelectResult = (
|
export let onSelect: (item: SelectionOrText, kb: boolean) => OnSelectResult = (
|
||||||
|
@ -195,7 +196,7 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-grow" use:outclick on:outclick={close}>
|
<div class="flex-grow {cls}" use:outclick on:outclick={close}>
|
||||||
<input
|
<input
|
||||||
class="w-full bg-transparent"
|
class="w-full bg-transparent"
|
||||||
class:px-2={padding}
|
class:px-2={padding}
|
||||||
|
|
|
@ -40,8 +40,8 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load query data if present
|
// Load query data if present
|
||||||
$: if (filterData) {
|
$: if (filterData || !filterData) {
|
||||||
updateFromQueryData(filterData);
|
updateFromQueryData(filterData ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFromQueryData(filterData: FilterQdata) {
|
function updateFromQueryData(filterData: FilterQdata) {
|
||||||
|
@ -154,7 +154,7 @@
|
||||||
|
|
||||||
<div class="flex flex-col items-start justify-center gap-2 relative">
|
<div class="flex flex-col items-start justify-center gap-2 relative">
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap w-full items-stretch h-auto p-0 gap-2 input input-sm input-bordered"
|
class="flex flex-wrap w-full items-stretch h-auto p-0 gap-2 input input-sm input-bordered relative"
|
||||||
>
|
>
|
||||||
{#each activeFilters as fdata, i}
|
{#each activeFilters as fdata, i}
|
||||||
<EntryFilterChip
|
<EntryFilterChip
|
||||||
|
@ -171,6 +171,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
bind:this={autocomplete}
|
bind:this={autocomplete}
|
||||||
|
cls="mr-8"
|
||||||
items={filterMenuItems}
|
items={filterMenuItems}
|
||||||
{hiddenIds}
|
{hiddenIds}
|
||||||
placeholder="Filter"
|
placeholder="Filter"
|
||||||
|
@ -185,7 +186,7 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-circle btn-ghost"
|
class="btn btn-sm btn-circle btn-ghost absolute bottom-0 right-0"
|
||||||
aria-label="Alle Filter entfernen"
|
aria-label="Alle Filter entfernen"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
activeFilters = [];
|
activeFilters = [];
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { trpc } from "$lib/shared/trpc";
|
||||||
import {
|
import {
|
||||||
mdiAccount,
|
mdiAccount,
|
||||||
mdiAccountInjury,
|
mdiAccountInjury,
|
||||||
|
mdiAccountRemoveOutline,
|
||||||
mdiBedKingOutline,
|
mdiBedKingOutline,
|
||||||
mdiCheckboxBlankOutline,
|
mdiCheckboxBlankOutline,
|
||||||
mdiCheckboxOutline,
|
mdiCheckboxOutline,
|
||||||
|
@ -91,3 +92,36 @@ export const ENTRY_FILTERS: { [key: string]: FilterDef } = {
|
||||||
inputType: 1,
|
inputType: 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PATIENT_FILTER: { [key: string]: FilterDef } = {
|
||||||
|
search: {
|
||||||
|
id: "search",
|
||||||
|
name: "Suche",
|
||||||
|
icon: mdiMagnify,
|
||||||
|
inputType: 1,
|
||||||
|
},
|
||||||
|
station: {
|
||||||
|
id: "station",
|
||||||
|
name: "Station",
|
||||||
|
icon: mdiDomain,
|
||||||
|
inputType: 2,
|
||||||
|
options: async () => {
|
||||||
|
return await trpc().station.list.query();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room: {
|
||||||
|
id: "room",
|
||||||
|
name: "Zimmer",
|
||||||
|
icon: mdiBedKingOutline,
|
||||||
|
inputType: 2,
|
||||||
|
options: async () => {
|
||||||
|
return await trpc().room.list.query();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
id: "hidden",
|
||||||
|
name: "Ausgeblendet",
|
||||||
|
icon: mdiAccountRemoveOutline,
|
||||||
|
inputType: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -33,10 +33,17 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each entries.items as entry}
|
{#each entries.items as entry}
|
||||||
<tr
|
<tr
|
||||||
|
class="transition-colors hover:bg-neutral-content/10"
|
||||||
class:done={entry.execution}
|
class:done={entry.execution}
|
||||||
class:priority={entry.current_version.priority}
|
class:priority={entry.current_version.priority}
|
||||||
>
|
>
|
||||||
<td>{entry.id}</td>
|
<td
|
||||||
|
><a
|
||||||
|
class="btn btn-xs btn-primary"
|
||||||
|
href="/entry/{entry.id}"
|
||||||
|
aria-label="Eintrag anzeigen">{entry.id}</a
|
||||||
|
></td
|
||||||
|
>
|
||||||
<td><PatientField patient={entry.patient} /></td>
|
<td><PatientField patient={entry.patient} /></td>
|
||||||
<td
|
<td
|
||||||
>{#if entry.patient.room}<RoomField room={entry.patient.room} />{/if}</td
|
>{#if entry.patient.room}<RoomField room={entry.patient.room} />{/if}</td
|
||||||
|
|
|
@ -19,4 +19,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button on:click={onClick}>{`${patient.first_name} ${patient.last_name}`}</button>
|
<button class="ellipsis" on:click={onClick}
|
||||||
|
>{`${patient.first_name} ${patient.last_name}`}</button
|
||||||
|
>
|
||||||
|
|
64
src/lib/components/table/PatientTable.svelte
Normal file
64
src/lib/components/table/PatientTable.svelte
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { SortRequest } from "$lib/shared/model";
|
||||||
|
import type { RouterOutput } from "$lib/shared/trpc";
|
||||||
|
import { formatDate } from "$lib/shared/util";
|
||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
import Icon from "../ui/Icon.svelte";
|
||||||
|
|
||||||
|
import RoomField from "./RoomField.svelte";
|
||||||
|
import SortHeader from "./SortHeader.svelte";
|
||||||
|
|
||||||
|
export let patients: RouterOutput["patient"]["list"];
|
||||||
|
export let sortData: SortRequest | undefined = undefined;
|
||||||
|
export let sortUpdate: (sort: SortRequest) => void = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<SortHeader title="ID" key="id" {sortData} {sortUpdate} />
|
||||||
|
<SortHeader title="Name" key="name" {sortData} {sortUpdate} />
|
||||||
|
<SortHeader title="Alter" key="age" {sortData} {sortUpdate} />
|
||||||
|
<SortHeader title="Zimmer" key="room" {sortData} {sortUpdate} />
|
||||||
|
<SortHeader title="Erstellt am" key="created_at" {sortData} {sortUpdate} />
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each patients.items as patient}
|
||||||
|
<tr
|
||||||
|
class="transition-colors hover:bg-neutral-content/10"
|
||||||
|
class:hidden={patient.hidden}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
><a
|
||||||
|
class="btn btn-xs btn-primary"
|
||||||
|
href="/patient/{patient.id}"
|
||||||
|
aria-label="Eintrag anzeigen">{patient.id}</a
|
||||||
|
></td
|
||||||
|
>
|
||||||
|
<td>{patient.first_name} {patient.last_name}</td>
|
||||||
|
<td>{patient.age}</td>
|
||||||
|
<td>
|
||||||
|
{#if patient.room}
|
||||||
|
<RoomField room={patient.room} />
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>{formatDate(patient.created_at, true)}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-circle btn-ghost btn-xs"
|
||||||
|
><Icon size={1.2} path={mdiClose} /></button
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.hidden {
|
||||||
|
@apply bg-red-500/20;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -14,4 +14,4 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button on:click={onClick}>{user.name}</button>
|
<button class="ellipsis" on:click={onClick}>{user.name}</button>
|
||||||
|
|
34
src/lib/components/ui/ErrorMessage.svelte
Normal file
34
src/lib/components/ui/ErrorMessage.svelte
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { TRPCErrorResponse } from "$lib/shared/model";
|
||||||
|
|
||||||
|
export let error: TRPCErrorResponse;
|
||||||
|
export let status: number | undefined = undefined;
|
||||||
|
export let withBtn = false;
|
||||||
|
|
||||||
|
$: statusCode = status ?? error.data?.httpStatus;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if statusCode === 404}
|
||||||
|
<div class="card m-4 bg-primary text-primary-content">
|
||||||
|
<div class="card-body items-center">
|
||||||
|
<h2 class="card-title">Fehler 404:</h2>
|
||||||
|
<p>Die Seite wurde nicht gefunden</p>
|
||||||
|
{#if withBtn}
|
||||||
|
<a class="btn" href="/">Zur Startseite</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="card m-4 bg-error text-error-content">
|
||||||
|
<div class="card-body items-center">
|
||||||
|
<h2 class="card-title">Fehler {statusCode}:</h2>
|
||||||
|
<p>{error.message}</p>
|
||||||
|
{#if withBtn}
|
||||||
|
{#if statusCode && statusCode < 500}
|
||||||
|
<a class="btn" href="/">Zur Startseite</a>
|
||||||
|
{:else}
|
||||||
|
<a class="btn" data-sveltekit-reload href=".">Neu laden</a>{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
60
src/lib/components/ui/LoadingBar.svelte
Normal file
60
src/lib/components/ui/LoadingBar.svelte
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let cls = "";
|
||||||
|
export let alwaysShown = false;
|
||||||
|
|
||||||
|
let navprogress = 0;
|
||||||
|
let navInterval: ReturnType<typeof setInterval>;
|
||||||
|
let showProgress = false;
|
||||||
|
let showError = false;
|
||||||
|
|
||||||
|
export function start() {
|
||||||
|
navprogress = 5;
|
||||||
|
showProgress = true;
|
||||||
|
showError = false;
|
||||||
|
navInterval = setInterval(() => {
|
||||||
|
navprogress += navprogress <= 90 ? 2.5 : navprogress < 95 ? 0.1 : 0;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reset() {
|
||||||
|
clearInterval(navInterval);
|
||||||
|
navprogress = 100;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
showProgress = false;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
navprogress = 0;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function error() {
|
||||||
|
showError = true;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={cls}
|
||||||
|
style:width={`${navprogress}%`}
|
||||||
|
class:active={alwaysShown || showProgress}
|
||||||
|
class:error={showError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
div {
|
||||||
|
height: 0;
|
||||||
|
transition-property: width, height;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
height: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
@apply bg-error !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,71 +0,0 @@
|
||||||
/*
|
|
||||||
import { ErrorConflict, ErrorNotFound } from "$lib/shared/util/error";
|
|
||||||
import { ZodError } from "zod";
|
|
||||||
import {
|
|
||||||
Ok,
|
|
||||||
BadRequest,
|
|
||||||
NotFound,
|
|
||||||
Conflict,
|
|
||||||
InternalServerError,
|
|
||||||
} from "sveltekit-zero-api/http";
|
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
||||||
import { userId } from "./util";
|
|
||||||
import type { RequestEvent } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
function handleError(error: unknown) {
|
|
||||||
if (error instanceof ZodError) {
|
|
||||||
// return { status: 400, msg: "Invalid input", data: error.flatten() };
|
|
||||||
return BadRequest({
|
|
||||||
body: { status: 400, msg: "Invalid input", data: error.flatten() },
|
|
||||||
});
|
|
||||||
} else if (error instanceof PrismaClientKnownRequestError) {
|
|
||||||
if (error.code === "P2025") {
|
|
||||||
return NotFound({
|
|
||||||
body: { status: 404, msg: error.message },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return InternalServerError({
|
|
||||||
body: { status: 500, msg: error.message, prismaCode: error.code },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (error instanceof ErrorNotFound) {
|
|
||||||
return NotFound({
|
|
||||||
body: { status: 404, msg: error.message },
|
|
||||||
});
|
|
||||||
} else if (error instanceof ErrorConflict) {
|
|
||||||
return Conflict({
|
|
||||||
body: { status: 409, msg: error.message },
|
|
||||||
});
|
|
||||||
} else if (error instanceof Error) {
|
|
||||||
return InternalServerError({
|
|
||||||
body: { status: 500, msg: error.message },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return InternalServerError({
|
|
||||||
body: { status: 500, msg: "unknown error" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function apiWrap<T>(f: () => Promise<T>) {
|
|
||||||
try {
|
|
||||||
const body = await f();
|
|
||||||
return Ok({ body });
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function apiWrapUser<T>(
|
|
||||||
event: RequestEvent,
|
|
||||||
f: (uId: number) => Promise<T>
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const uId = await userId(event);
|
|
||||||
const body = await f(uId);
|
|
||||||
return Ok({ body });
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
42
src/lib/server/trpc/handleError.ts
Normal file
42
src/lib/server/trpc/handleError.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { ErrorConflict, ErrorNotFound } from "$lib/shared/util/error";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
function handleError(error: unknown): void {
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
// return { status: 400, msg: "Invalid input", data: error.flatten() };
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Invalid input",
|
||||||
|
cause: error.flatten(),
|
||||||
|
});
|
||||||
|
} else if (error instanceof PrismaClientKnownRequestError) {
|
||||||
|
if (error.code === "P2025") {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: error.message });
|
||||||
|
} else {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: error.message,
|
||||||
|
cause: "Prisma error " + error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (error instanceof ErrorNotFound) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: error.message });
|
||||||
|
} else if (error instanceof ErrorConflict) {
|
||||||
|
throw new TRPCError({ code: "CONFLICT", message: error.message });
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: error.message });
|
||||||
|
} else {
|
||||||
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "unknown error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wrap a TRPC query to handle occuring errors */
|
||||||
|
export async function trpcWrap<T>(f: () => Promise<T>) {
|
||||||
|
try {
|
||||||
|
return await f();
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
import { initTRPC } from "@trpc/server";
|
import { initTRPC } from "@trpc/server";
|
||||||
import type { Context } from "./context";
|
import type { Context } from "./context";
|
||||||
|
|
||||||
|
export { trpcWrap } from "$lib/server/trpc/handleError";
|
||||||
|
|
||||||
export const t = initTRPC.context<Context>().create();
|
export const t = initTRPC.context<Context>().create();
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
|
import type { Renderers } from "svelte-markdown";
|
||||||
|
|
||||||
export const PAGINATION_LIMIT = 20;
|
export const PAGINATION_LIMIT = 20;
|
||||||
|
export const MARKDOWN_RENDERERS: Partial<Renderers> = { image: undefined };
|
||||||
|
|
|
@ -34,3 +34,13 @@ export type PatientsFilter = Partial<{
|
||||||
room: FilterList<number>;
|
room: FilterList<number>;
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type TRPCErrorResponse = {
|
||||||
|
message: string;
|
||||||
|
data?: {
|
||||||
|
code: string;
|
||||||
|
httpStatus: number;
|
||||||
|
stack?: string;
|
||||||
|
path?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
||||||
} from ".";
|
} from ".";
|
||||||
|
|
||||||
export const ZEntityId = z.number().int().nonnegative();
|
export const ZEntityId = z.number().int().nonnegative();
|
||||||
|
export const ZUrlEntityId = z.coerce.number().int().nonnegative();
|
||||||
const ZNameString = z.string().min(1).max(200).trim();
|
const ZNameString = z.string().min(1).max(200).trim();
|
||||||
const ZTextString = z.string().trim();
|
const ZTextString = z.string().trim();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import type { EntityQuery } from "$lib/shared/model";
|
import type { EntityQuery } from "$lib/shared/model";
|
||||||
|
import { TRPCClientError } from "@trpc/client";
|
||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
export function formatDate(date: Date | string, time = false): string {
|
export function formatDate(date: Date | string, time = false): string {
|
||||||
let dt = date;
|
let dt = date;
|
||||||
|
@ -23,14 +25,13 @@ export function formatDate(date: Date | string, time = false): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getQueryUrl(q: EntityQuery, basePath: string = ""): string {
|
export function getQueryUrl(q: EntityQuery, basePath: string): string {
|
||||||
return basePath + "?q=" + JSON.stringify(q);
|
return basePath + "/" + JSON.stringify(q);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gotoEntityQuery(q: EntityQuery, basePath: string) {
|
export function gotoEntityQuery(q: EntityQuery, basePath: string) {
|
||||||
if (window && basePath === window.location.pathname) {
|
if (window && window.location.pathname.startsWith(basePath + "/")) {
|
||||||
const ws = new URLSearchParams(window.location.search);
|
const qstr = decodeURI(window.location.pathname.substring(basePath.length + 1));
|
||||||
const qstr = ws.get("q");
|
|
||||||
if (qstr) {
|
if (qstr) {
|
||||||
const oldq: EntityQuery = JSON.parse(qstr);
|
const oldq: EntityQuery = JSON.parse(qstr);
|
||||||
const nq: EntityQuery = {
|
const nq: EntityQuery = {
|
||||||
|
@ -43,3 +44,21 @@ export function gotoEntityQuery(q: EntityQuery, basePath: string) {
|
||||||
}
|
}
|
||||||
goto(getQueryUrl(q, basePath));
|
goto(getQueryUrl(q, basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wrap a page load query to handle occuring errors
|
||||||
|
*
|
||||||
|
* Converts TRPC errors to SvelteKit ones
|
||||||
|
*/
|
||||||
|
export async function loadWrap<T>(f: () => Promise<T>) {
|
||||||
|
try {
|
||||||
|
return await f();
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof TRPCClientError) {
|
||||||
|
error(e.data?.httpStatus ?? 500, e.message);
|
||||||
|
} else if (e instanceof Error) {
|
||||||
|
error(500, e.message);
|
||||||
|
} else {
|
||||||
|
error(500, "unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,8 +19,9 @@
|
||||||
cls={$page.route.id === "/(app)" ? "text-primary" : ""}
|
cls={$page.route.id === "/(app)" ? "text-primary" : ""}
|
||||||
/></NavLink
|
/></NavLink
|
||||||
>
|
>
|
||||||
<NavLink route="/(app)/plan" href="/plan">Planung</NavLink>
|
<NavLink route="/(app)/plan/[[query]]" href="/plan">Planung</NavLink>
|
||||||
<NavLink route="/(app)/visit" href="/visit">Visite</NavLink>
|
<NavLink route="/(app)/visit" href="/visit">Visite</NavLink>
|
||||||
|
<NavLink route="/(app)/patients/[[query]]" href="/patients">Patienten</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-0">
|
<div class="flex-0">
|
||||||
{#if $page.data.session?.user}
|
{#if $page.data.session?.user}
|
||||||
|
@ -34,6 +35,9 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
|
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
|
||||||
>
|
>
|
||||||
|
<li><a href="/rooms">Zimmer</a></li>
|
||||||
|
<li><a href="/categories">Kategorien</a></li>
|
||||||
|
<li><a href="/users">Benutzer</a></li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if $page.data.session?.user}
|
{#if $page.data.session?.user}
|
||||||
<h1 class="text-2xl font-bold">Hallo, {$page.data.session.user.name}</h1>
|
<h1 class="heading">Hallo, {$page.data.session.user.name}</h1>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Sie sind nicht angemeldet</p>
|
<p>Sie sind nicht angemeldet</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -35,4 +35,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a href="/plan/%7B%22filter%22:%7B%22done%22:%22foo%22%7D%7D">Throw error</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
69
src/routes/(app)/entry/[id]/+page.svelte
Normal file
69
src/routes/(app)/entry/[id]/+page.svelte
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import SvelteMarkdown from "svelte-markdown";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
import { formatDate } from "$lib/shared/util";
|
||||||
|
import UserField from "$lib/components/table/UserField.svelte";
|
||||||
|
import RoomField from "$lib/components/table/RoomField.svelte";
|
||||||
|
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
||||||
|
import { MARKDOWN_RENDERERS } from "$lib/shared/constants";
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Eintrag #{data.entry.id}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<h1 class="heading">
|
||||||
|
Eintrag #{data.entry.id}
|
||||||
|
{#if data.entry.current_version.category}
|
||||||
|
<CategoryField category={data.entry.current_version.category} />
|
||||||
|
{/if}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<p>
|
||||||
|
{#if data.entry.patient.room}
|
||||||
|
<RoomField room={data.entry.patient.room} />
|
||||||
|
{/if}
|
||||||
|
{data.entry.patient.first_name}
|
||||||
|
{data.entry.patient.last_name}
|
||||||
|
({data.entry.patient.age})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<p class="prose">
|
||||||
|
<SvelteMarkdown
|
||||||
|
source={data.entry.current_version.text}
|
||||||
|
renderers={MARKDOWN_RENDERERS}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if data.entry.execution}
|
||||||
|
<div class="box">
|
||||||
|
<p>
|
||||||
|
Erledigt am {formatDate(data.entry.execution.created_at, true)} von
|
||||||
|
<UserField user={data.entry.execution.author} filterName="executor" />:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="prose">
|
||||||
|
<SvelteMarkdown
|
||||||
|
source={data.entry.execution?.text}
|
||||||
|
renderers={MARKDOWN_RENDERERS}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
p {
|
||||||
|
@apply my-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
@apply rounded-xl border-solid border-2 border-base-content/40;
|
||||||
|
@apply px-2 my-4;
|
||||||
|
}
|
||||||
|
</style>
|
11
src/routes/(app)/entry/[id]/+page.ts
Normal file
11
src/routes/(app)/entry/[id]/+page.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||||
|
import { trpc } from "$lib/shared/trpc";
|
||||||
|
import { loadWrap } from "$lib/shared/util";
|
||||||
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
|
export const load: PageLoad = async (event) => {
|
||||||
|
const id = ZUrlEntityId.parse(event.params.id);
|
||||||
|
const entry = await loadWrap(async () => trpc(event).entry.get.query(id));
|
||||||
|
|
||||||
|
return { entry };
|
||||||
|
};
|
|
@ -2,14 +2,14 @@
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
import EntryTable from "$lib/components/table/EntryTable.svelte";
|
|
||||||
import PaginationButtons from "$lib/components/ui/PaginationButtons.svelte";
|
import PaginationButtons from "$lib/components/ui/PaginationButtons.svelte";
|
||||||
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
||||||
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||||
import type { FilterQdata } from "$lib/components/filter/types";
|
import type { FilterQdata } from "$lib/components/filter/types";
|
||||||
import { trpc } from "$lib/shared/trpc";
|
import { trpc } from "$lib/shared/trpc";
|
||||||
import { ENTRY_FILTERS } from "$lib/components/filter/filters";
|
import { ENTRY_FILTERS, PATIENT_FILTER } from "$lib/components/filter/filters";
|
||||||
import { getQueryUrl } from "$lib/shared/util";
|
import { getQueryUrl } from "$lib/shared/util";
|
||||||
|
import PatientTable from "$lib/components/table/PatientTable.svelte";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
@ -39,14 +39,14 @@
|
||||||
if (browser) {
|
if (browser) {
|
||||||
// Update page URL (not using the Svelte Router here because
|
// Update page URL (not using the Svelte Router here because
|
||||||
// we do not need to reload the page)
|
// we do not need to reload the page)
|
||||||
const url = getQueryUrl(query);
|
const url = getQueryUrl(query, "/patients");
|
||||||
window.history.replaceState(null, "", url);
|
window.history.replaceState(null, "", url);
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
trpc()
|
trpc()
|
||||||
.entry.list.query(query)
|
.patient.list.query(query)
|
||||||
.then((entries) => {
|
.then((patients) => {
|
||||||
data.entries = entries;
|
data.patients = patients;
|
||||||
data.query = query;
|
data.query = query;
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
@ -55,11 +55,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Visitenbuch - Planung</title>
|
<title>Patienten</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<FilterBar
|
<FilterBar
|
||||||
FILTERS={ENTRY_FILTERS}
|
FILTERS={PATIENT_FILTER}
|
||||||
filterData={data.query.filter}
|
filterData={data.query.filter}
|
||||||
onUpdate={filterUpdate}
|
onUpdate={filterUpdate}
|
||||||
/>
|
/>
|
||||||
|
@ -68,10 +68,10 @@
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<EntryTable entries={data.entries} sortData={data.query.sort} {sortUpdate} />
|
<PatientTable patients={data.patients} sortData={data.query.sort} {sortUpdate} />
|
||||||
|
|
||||||
<PaginationButtons
|
<PaginationButtons
|
||||||
paginationData={data.query.pagination}
|
paginationData={data.query.pagination}
|
||||||
data={data.entries}
|
data={data.patients}
|
||||||
onUpdate={paginationUpdate}
|
onUpdate={paginationUpdate}
|
||||||
/>
|
/>
|
18
src/routes/(app)/patients/[[query]]/+page.ts
Normal file
18
src/routes/(app)/patients/[[query]]/+page.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ZPatientsQuery } from "$lib/shared/model/validation";
|
||||||
|
import { trpc } from "$lib/shared/trpc";
|
||||||
|
import type { PageLoad } from "./$types";
|
||||||
|
import { loadWrap } from "$lib/shared/util";
|
||||||
|
|
||||||
|
export const load: PageLoad = async (event) => {
|
||||||
|
let query: z.infer<typeof ZPatientsQuery> = {};
|
||||||
|
|
||||||
|
if (event.params.query) {
|
||||||
|
query = ZPatientsQuery.parse(JSON.parse(event.params.query));
|
||||||
|
}
|
||||||
|
|
||||||
|
const patients = await loadWrap(async () => trpc(event).patient.list.query(query));
|
||||||
|
|
||||||
|
return { query, patients };
|
||||||
|
};
|
|
@ -1,18 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
|
||||||
import { trpc } from "$lib/shared/trpc";
|
|
||||||
import type { PageLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load: PageLoad = async (event) => {
|
|
||||||
const q = event.url.searchParams.get("q");
|
|
||||||
let query: z.infer<typeof ZEntriesQuery> = {};
|
|
||||||
|
|
||||||
if (q) {
|
|
||||||
query = ZEntriesQuery.parse(JSON.parse(q));
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await trpc(event).entry.list.query(query);
|
|
||||||
|
|
||||||
return { query, entries };
|
|
||||||
};
|
|
90
src/routes/(app)/plan/[[query]]/+page.svelte
Normal file
90
src/routes/(app)/plan/[[query]]/+page.svelte
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import type { PageData } from "../$types";
|
||||||
|
|
||||||
|
import EntryTable from "$lib/components/table/EntryTable.svelte";
|
||||||
|
import PaginationButtons from "$lib/components/ui/PaginationButtons.svelte";
|
||||||
|
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
||||||
|
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||||
|
import type { FilterQdata } from "$lib/components/filter/types";
|
||||||
|
import { trpc } from "$lib/shared/trpc";
|
||||||
|
import { ENTRY_FILTERS } from "$lib/components/filter/filters";
|
||||||
|
import { getQueryUrl } from "$lib/shared/util";
|
||||||
|
import LoadingBar from "$lib/components/ui/LoadingBar.svelte";
|
||||||
|
import ErrorMessage from "$lib/components/ui/ErrorMessage.svelte";
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
let loadingBar: LoadingBar | undefined;
|
||||||
|
let error: Error | null = null;
|
||||||
|
|
||||||
|
function paginationUpdate(pagination: PaginationRequest) {
|
||||||
|
updateQuery({
|
||||||
|
filter: data.query.filter,
|
||||||
|
pagination,
|
||||||
|
sort: data.query.sort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterUpdate(filter: FilterQdata) {
|
||||||
|
updateQuery({ filter, sort: data.query.sort });
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortUpdate(sort: SortRequest) {
|
||||||
|
updateQuery({
|
||||||
|
filter: data.query.filter,
|
||||||
|
pagination: data.query.pagination,
|
||||||
|
sort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQuery(query: typeof data.query) {
|
||||||
|
if (browser) {
|
||||||
|
// Update page URL (not using the Svelte Router here because
|
||||||
|
// we do not need to reload the page)
|
||||||
|
const url = getQueryUrl(query, "/plan");
|
||||||
|
window.history.replaceState(null, "", url);
|
||||||
|
|
||||||
|
loadingBar?.start();
|
||||||
|
trpc()
|
||||||
|
.entry.list.query(query)
|
||||||
|
.then((entries) => {
|
||||||
|
data.entries = entries;
|
||||||
|
error = null;
|
||||||
|
loadingBar?.reset();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
data.query = query;
|
||||||
|
error = err;
|
||||||
|
loadingBar?.error();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
data.query = query;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Planung</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<FilterBar
|
||||||
|
FILTERS={ENTRY_FILTERS}
|
||||||
|
filterData={data.query.filter}
|
||||||
|
onUpdate={filterUpdate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingBar bind:this={loadingBar} alwaysShown />
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<ErrorMessage {error} />
|
||||||
|
{:else}
|
||||||
|
<EntryTable entries={data.entries} sortData={data.query.sort} {sortUpdate} />
|
||||||
|
|
||||||
|
<PaginationButtons
|
||||||
|
paginationData={data.query.pagination}
|
||||||
|
data={data.entries}
|
||||||
|
onUpdate={paginationUpdate}
|
||||||
|
/>
|
||||||
|
{/if}
|
15
src/routes/(app)/plan/[[query]]/+page.ts
Normal file
15
src/routes/(app)/plan/[[query]]/+page.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { trpc } from "$lib/shared/trpc";
|
||||||
|
import { loadWrap } from "$lib/shared/util";
|
||||||
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
|
export const load: PageLoad = async (event) => {
|
||||||
|
let query = {};
|
||||||
|
|
||||||
|
if (event.params.query) {
|
||||||
|
query = JSON.parse(event.params.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await loadWrap(() => trpc(event).entry.list.query(query));
|
||||||
|
|
||||||
|
return { query, entries };
|
||||||
|
};
|
|
@ -2,7 +2,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Visitenbuch - Visite</title>
|
<title>Visite</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 class="text-4xl">Visite</h1>
|
<h1 class="text-4xl">Visite</h1>
|
||||||
|
|
15
src/routes/+error.svelte
Normal file
15
src/routes/+error.svelte
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
|
||||||
|
import ErrorMessage from "$lib/components/ui/ErrorMessage.svelte";
|
||||||
|
|
||||||
|
console.log("Fehlerhafte Seite", $page);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Visitenbuch: Fehler {$page.status}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if $page.error}
|
||||||
|
<ErrorMessage error={$page.error} status={$page.status} withBtn />
|
||||||
|
{/if}
|
|
@ -1,7 +1,23 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "../app.pcss"; // eslint-disable-line no-relative-import-paths/no-relative-import-paths
|
import "../app.pcss"; // eslint-disable-line no-relative-import-paths/no-relative-import-paths
|
||||||
|
|
||||||
|
import { afterNavigate, beforeNavigate } from "$app/navigation";
|
||||||
|
|
||||||
|
import LoadingBar from "$lib/components/ui/LoadingBar.svelte";
|
||||||
|
|
||||||
|
let loadingBar: LoadingBar | undefined;
|
||||||
|
|
||||||
|
beforeNavigate(() => {
|
||||||
|
if (loadingBar) loadingBar.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterNavigate(() => {
|
||||||
|
if (loadingBar) loadingBar.reset();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<LoadingBar cls="fixed top-0 left-0 z-50" bind:this={loadingBar} />
|
||||||
|
|
||||||
<div class="bg-base-100 text-base-content">
|
<div class="bg-base-100 text-base-content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,7 +15,7 @@ const CATEGORY_IDS: { [id: string]: number } = {
|
||||||
Entlassung: 5,
|
Entlassung: 5,
|
||||||
Sonstiges: 6,
|
Sonstiges: 6,
|
||||||
};
|
};
|
||||||
const refDate = new Date(2023, 11, 1);
|
const refDate = new Date(Date.UTC(2023, 11, 1));
|
||||||
|
|
||||||
const N_USERS = 10;
|
const N_USERS = 10;
|
||||||
const N_ROOMS = 20;
|
const N_ROOMS = 20;
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { expect, test } from "vitest";
|
||||||
const TEST_VERSION = {
|
const TEST_VERSION = {
|
||||||
category_id: 1,
|
category_id: 1,
|
||||||
text: "10ml Blut abnehmen",
|
text: "10ml Blut abnehmen",
|
||||||
date: new Date(2024, 1, 1),
|
date: new Date(Date.UTC(2024, 1, 1)),
|
||||||
priority: false,
|
priority: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ test("create entry version", async () => {
|
||||||
version: TEST_VERSION,
|
version: TEST_VERSION,
|
||||||
});
|
});
|
||||||
const text = "10ml Blut abnehmen\n\nPS: Nadel nicht vergessen";
|
const text = "10ml Blut abnehmen\n\nPS: Nadel nicht vergessen";
|
||||||
const date = new Date(2024, 1, 2);
|
const date = new Date(Date.UTC(2024, 1, 2));
|
||||||
|
|
||||||
await newEntryVersion(1, eId, {
|
await newEntryVersion(1, eId, {
|
||||||
date,
|
date,
|
||||||
|
@ -155,7 +155,7 @@ async function insertTestEntries() {
|
||||||
patient_id: 2,
|
patient_id: 2,
|
||||||
version: {
|
version: {
|
||||||
text: "Carrot cake jelly-o bonbon toffee chocolate.",
|
text: "Carrot cake jelly-o bonbon toffee chocolate.",
|
||||||
date: new Date(2024, 1, 5),
|
date: new Date(Date.UTC(2024, 1, 5)),
|
||||||
priority: false,
|
priority: false,
|
||||||
category_id: null,
|
category_id: null,
|
||||||
},
|
},
|
||||||
|
@ -164,7 +164,7 @@ async function insertTestEntries() {
|
||||||
patient_id: 1,
|
patient_id: 1,
|
||||||
version: {
|
version: {
|
||||||
text: "Cheesecake danish donut oat cake caramels.",
|
text: "Cheesecake danish donut oat cake caramels.",
|
||||||
date: new Date(2024, 1, 6),
|
date: new Date(Date.UTC(2024, 1, 6)),
|
||||||
priority: false,
|
priority: false,
|
||||||
category_id: null,
|
category_id: null,
|
||||||
},
|
},
|
||||||
|
@ -174,7 +174,7 @@ async function insertTestEntries() {
|
||||||
await newEntryVersion(2, eId1, {
|
await newEntryVersion(2, eId1, {
|
||||||
category_id: 3,
|
category_id: 3,
|
||||||
text: TEST_VERSION.text + "\n\n> Hello World",
|
text: TEST_VERSION.text + "\n\n> Hello World",
|
||||||
date: new Date(2024, 1, 1),
|
date: new Date(Date.UTC(2024, 1, 1)),
|
||||||
priority: true,
|
priority: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue