Compare commits

..

No commits in common. "6c338e447e886f45f371103a10e0ae8c94199c76" and "c2c21d1296c399324cdfa661c490cee79f7e16e1" have entirely different histories.

39 changed files with 185 additions and 324 deletions

View file

@ -3,14 +3,6 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [v0.2.1](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.2.0..v0.2.1) - 2024-05-07
### 🐛 Bug Fixes
- Carta editor mobile context menu transparent - ([6bd7e15](https://code.thetadev.de/HSA/Visitenbuch/commit/6bd7e157eea6589be82692499cb956455386f7e5))
- Dont output null age - ([c2c21d1](https://code.thetadev.de/HSA/Visitenbuch/commit/c2c21d1296c399324cdfa661c490cee79f7e16e1))
## [v0.2.0](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.1.0..v0.2.0) - 2024-05-06 ## [v0.2.0](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.1.0..v0.2.0) - 2024-05-06
### 🚀 Features ### 🚀 Features

View file

@ -1,6 +1,6 @@
{ {
"name": "visitenbuch", "name": "visitenbuch",
"version": "0.2.1", "version": "0.2.0",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -21,7 +21,7 @@
"@floating-ui/core": "^1.6.1", "@floating-ui/core": "^1.6.1",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@prisma/client": "^5.13.0", "@prisma/client": "^5.13.0",
"@thetadev/carta-md": "^1.0.1", "carta-md": "4.0.2",
"diff": "^5.2.0", "diff": "^5.2.0",
"isomorphic-dompurify": "^2.9.0", "isomorphic-dompurify": "^2.9.0",
"prisma": "^5.13.0", "prisma": "^5.13.0",
@ -71,5 +71,10 @@
"vite": "^5.2.11", "vite": "^5.2.11",
"vitest": "^1.6.0" "vitest": "^1.6.0"
}, },
"type": "module" "type": "module",
"pnpm": {
"patchedDependencies": {
"carta-md@4.0.2": "patches/carta-md@4.0.2.patch"
}
}
} }

View file

@ -0,0 +1,27 @@
# Change "markdown-body" css class to "prose" for Tailwind compatibility
diff --git a/dist/Markdown.svelte b/dist/Markdown.svelte
index 92b29fade303a14539720b9bc389e7a41202b1cf..cbdeede16d7af17a481bcac519aea92b8959803d 100644
--- a/dist/Markdown.svelte
+++ b/dist/Markdown.svelte
@@ -15,7 +15,7 @@ onMount(async () => {
});
</script>
-<div bind:this={elem} class="carta-viewer carta-theme__{theme} markdown-body">
+<div bind:this={elem} class="carta-viewer carta-theme__{theme} prose">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html rendered}
{#if mounted}
diff --git a/dist/internal/components/Renderer.svelte b/dist/internal/components/Renderer.svelte
index 1d2ff1a6937bc490ea1e6eb5c2ef9f3b33e4c326..6a95c154ea4ca7c4d19b20d02b3504fb2b65b7f7 100644
--- a/dist/internal/components/Renderer.svelte
+++ b/dist/internal/components/Renderer.svelte
@@ -17,7 +17,7 @@ onMount(() => carta.$setRenderer(elem));
onMount(() => mounted = true);
</script>
-<div bind:this={elem} on:scroll={handleScroll} class="carta-renderer markdown-body">
+<div bind:this={elem} on:scroll={handleScroll} class="carta-renderer prose">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html renderedHtml}
{#if mounted}

View file

@ -4,6 +4,11 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
patchedDependencies:
carta-md@4.0.2:
hash: i33ea43vfgrg3ziu25cfu7s2zq
path: patches/carta-md@4.0.2.patch
dependencies: dependencies:
'@auth/core': '@auth/core':
specifier: ^0.30.0 specifier: ^0.30.0
@ -17,9 +22,9 @@ dependencies:
'@prisma/client': '@prisma/client':
specifier: ^5.13.0 specifier: ^5.13.0
version: 5.13.0(prisma@5.13.0) version: 5.13.0(prisma@5.13.0)
'@thetadev/carta-md': carta-md:
specifier: ^1.0.1 specifier: 4.0.2
version: 1.0.1(svelte@4.2.15) version: 4.0.2(patch_hash=i33ea43vfgrg3ziu25cfu7s2zq)(svelte@4.2.15)
diff: diff:
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.2.0 version: 5.2.0
@ -884,6 +889,10 @@ packages:
dev: true dev: true
optional: true optional: true
/@shikijs/core@1.4.0:
resolution: {integrity: sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==}
dev: false
/@sideway/address@4.1.5: /@sideway/address@4.1.5:
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
requiresBuild: true requiresBuild: true
@ -1079,22 +1088,6 @@ packages:
tailwindcss: 3.4.3 tailwindcss: 3.4.3
dev: true dev: true
/@thetadev/carta-md@1.0.1(svelte@4.2.15):
resolution: {integrity: sha512-B6TyB5gvrc1T5pEOM5nIZA+N6gG7EK7AJBrQCfokzrpNarq7CvgJEVaQoWh5oukB2MrRYSIQAYUNIoqEKzMSag==}
peerDependencies:
svelte: ^3.54.0 || ^4.0.0
dependencies:
prismjs: 1.29.0
rehype-stringify: 10.0.0
remark-gfm: 4.0.0
remark-parse: 11.0.0
remark-rehype: 11.1.0
svelte: 4.2.15
unified: 11.0.4
transitivePeerDependencies:
- supports-color
dev: false
/@trpc/client@10.45.2(@trpc/server@10.45.2): /@trpc/client@10.45.2(@trpc/server@10.45.2):
resolution: {integrity: sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg==} resolution: {integrity: sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg==}
peerDependencies: peerDependencies:
@ -1764,6 +1757,23 @@ packages:
resolution: {integrity: sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==} resolution: {integrity: sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==}
dev: true dev: true
/carta-md@4.0.2(patch_hash=i33ea43vfgrg3ziu25cfu7s2zq)(svelte@4.2.15):
resolution: {integrity: sha512-wMlw0r5RZiVwvF3dyxE/vHj9pXlXbzpijJ3m/o9zqZe7Cf6D96AjyBHBpa0A0OPj/uEJVF3k0R6ctopBJCpCQg==}
peerDependencies:
svelte: ^3.54.0 || ^4.0.0
dependencies:
rehype-stringify: 10.0.0
remark-gfm: 4.0.0
remark-parse: 11.0.0
remark-rehype: 11.1.0
shiki: 1.4.0
svelte: 4.2.15
unified: 11.0.4
transitivePeerDependencies:
- supports-color
dev: false
patched: true
/ccount@2.0.1: /ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
dev: false dev: false
@ -4414,11 +4424,6 @@ packages:
'@prisma/engines': 5.13.0 '@prisma/engines': 5.13.0
dev: false dev: false
/prismjs@1.29.0:
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
engines: {node: '>=6'}
dev: false
/property-expr@2.0.6: /property-expr@2.0.6:
resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==}
requiresBuild: true requiresBuild: true
@ -4716,6 +4721,12 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/shiki@1.4.0:
resolution: {integrity: sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==}
dependencies:
'@shikijs/core': 1.4.0
dev: false
/side-channel@1.0.6: /side-channel@1.0.6:
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "entry_executions" ADD COLUMN "done" BOOLEAN NOT NULL DEFAULT true;

View file

@ -142,7 +142,6 @@ model EntryExecution {
entry_id Int entry_id Int
text String text String
done Boolean @default(true)
author User @relation(fields: [author_id], references: [id]) author User @relation(fields: [author_id], references: [id])
author_id Int author_id Int

View file

@ -39,7 +39,7 @@
<p class="text-sm flex gap-2"> <p class="text-sm flex 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?.done} {#if entry.execution}
<span>Erledigt {humanDate(entry.execution.created_at)}</span> <span>Erledigt {humanDate(entry.execution.created_at)}</span>
{:else} {:else}
<span>Zu erledigen {humanDate(entry.current_version.date)}</span> <span>Zu erledigen {humanDate(entry.current_version.date)}</span>
@ -53,7 +53,7 @@
Beschreibung Beschreibung
<div> <div>
<VersionsButton href="{basePath}/versions" n={entry.n_versions} /> <VersionsButton href="{basePath}/versions" n={entry.n_versions} />
<a class="btn btn-circle btn-xs btn-ghost" href="{basePath}/edit"> <a class="btn btn-circle btn-sm btn-ghost" href="{basePath}/edit">
<Icon path={mdiPencil} size={1.2} /> <Icon path={mdiPencil} size={1.2} />
</a> </a>
</div> </div>
@ -69,12 +69,9 @@
{#if withExecution && entry.execution} {#if withExecution && entry.execution}
<div class="card2"> <div class="card2">
<div <div class="row c-light text-sm items-center justify-between">
class="row c-light text-sm items-center justify-between"
class:done={entry.execution.done}
class:note={!entry.execution.done}>
<p> <p>
{entry.execution.done ? "Erledigt am" : "Notiz von"} {formatDate(entry.execution.created_at, true)} von Erledigt am {formatDate(entry.execution.created_at, true)} von
<UserField filterName="executor" user={entry.execution.author} /> <UserField filterName="executor" user={entry.execution.author} />
</p> </p>
<div> <div>
@ -91,12 +88,3 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<style lang="postcss">
.done {
@apply bg-success/20;
}
.note {
@apply bg-warning/20;
}
</style>

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { SortRequest } from "$lib/shared/model";
import type { RouterOutput } from "$lib/shared/trpc"; import type { RouterOutput } from "$lib/shared/trpc";
import { formatDate } from "$lib/shared/util"; import { formatDate } from "$lib/shared/util";
@ -9,8 +10,8 @@
import UserField from "./UserField.svelte"; import UserField from "./UserField.svelte";
export let entries: RouterOutput["entry"]["list"]; export let entries: RouterOutput["entry"]["list"];
export let sortData: string[] | undefined; export let sortData: SortRequest | undefined;
export let sortUpdate: (sort: string[] | undefined) => void = () => {}; export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
export let perPatient = false; export let perPatient = false;
export let baseUrl: string; export let baseUrl: string;
</script> </script>
@ -37,7 +38,7 @@
{#each entries.items as entry (entry.id)} {#each entries.items as entry (entry.id)}
<tr <tr
class="transition-colors hover:bg-neutral-content/10" class="transition-colors hover:bg-neutral-content/10"
class:done={entry.execution?.done} class:done={entry.execution}
class:priority={entry.current_version.priority} class:priority={entry.current_version.priority}
> >
<td <td
@ -66,7 +67,7 @@
<td><UserField {baseUrl} user={entry.current_version.author} /></td> <td><UserField {baseUrl} user={entry.current_version.author} /></td>
<td><span class="line-clamp-2">{entry.current_version.text}</span></td> <td><span class="line-clamp-2">{entry.current_version.text}</span></td>
<td> <td>
{#if entry.execution?.done} {#if entry.execution}
{formatDate(entry.execution.created_at, true)} {formatDate(entry.execution.created_at, true)}
<UserField <UserField
{baseUrl} {baseUrl}

View file

@ -5,7 +5,7 @@
import { z } from "zod"; import { z } from "zod";
import type { PaginationRequest } from "$lib/shared/model"; import type { PaginationRequest, SortRequest } from "$lib/shared/model";
import type { ZEntriesQuery } from "$lib/shared/model/validation"; import type { ZEntriesQuery } from "$lib/shared/model/validation";
import { type RouterOutput } from "$lib/shared/trpc"; import { type RouterOutput } from "$lib/shared/trpc";
import { getQueryUrl } from "$lib/shared/util"; import { getQueryUrl } from "$lib/shared/util";
@ -34,7 +34,7 @@
updateQuery({ filter, sort: query.sort }); updateQuery({ filter, sort: query.sort });
} }
function sortUpdate(sort: string[] | undefined): void { function sortUpdate(sort: SortRequest | undefined): void {
updateQuery({ updateQuery({
filter: query.filter, filter: query.filter,
pagination: query.pagination, pagination: query.pagination,

View file

@ -5,7 +5,7 @@
import { z } from "zod"; import { z } from "zod";
import type { PaginationRequest } from "$lib/shared/model"; import type { PaginationRequest, SortRequest } from "$lib/shared/model";
import type { ZPatientsQuery } from "$lib/shared/model/validation"; import type { ZPatientsQuery } from "$lib/shared/model/validation";
import { type RouterOutput } from "$lib/shared/trpc"; import { type RouterOutput } from "$lib/shared/trpc";
import { getQueryUrl } from "$lib/shared/util"; import { getQueryUrl } from "$lib/shared/util";
@ -32,7 +32,7 @@
updateQuery({ filter, sort: query.sort }); updateQuery({ filter, sort: query.sort });
} }
function sortUpdate(sort: string[] | undefined): void { function sortUpdate(sort: SortRequest | undefined): void {
updateQuery({ updateQuery({
filter: query.filter, filter: query.filter,
pagination: query.pagination, pagination: query.pagination,

View file

@ -2,6 +2,7 @@
import { mdiFilter } from "@mdi/js"; import { mdiFilter } from "@mdi/js";
import { URL_ENTRIES } from "$lib/shared/constants"; import { URL_ENTRIES } from "$lib/shared/constants";
import type { SortRequest } from "$lib/shared/model";
import type { RouterOutput } from "$lib/shared/trpc"; import type { RouterOutput } from "$lib/shared/trpc";
import { formatDate, gotoEntityQuery } from "$lib/shared/util"; import { formatDate, gotoEntityQuery } from "$lib/shared/util";
@ -11,8 +12,8 @@
import SortHeader from "./SortHeader.svelte"; import SortHeader from "./SortHeader.svelte";
export let patients: RouterOutput["patient"]["list"]; export let patients: RouterOutput["patient"]["list"];
export let sortData: string[] | undefined; export let sortData: SortRequest | undefined;
export let sortUpdate: (sort: string[] | undefined) => void = () => {}; export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
export let baseUrl: string; export let baseUrl: string;
</script> </script>

View file

@ -1,33 +1,29 @@
<script lang="ts"> <script lang="ts">
import { mdiSortAscending, mdiSortDescending } from "@mdi/js"; import { mdiSortAscending, mdiSortDescending } from "@mdi/js";
import type { SortRequest } from "$lib/shared/model";
import Icon from "$lib/components/ui/Icon.svelte"; import Icon from "$lib/components/ui/Icon.svelte";
export let key: string; export let key: string;
export let title: string; export let title: string;
export let sortData: string[] | undefined; export let sortData: SortRequest | undefined;
export let sortUpdate: (sort: string[] | undefined) => void = () => {}; export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
// 1: asc, 2: desc, 0: not sorted // 1: asc, 2: desc, 0: not sorted
let sorting = 0; let sorting = 0;
$: index = sortData?.findIndex((itm) => itm.split(":", 1)[0] === key) ?? -1; $: if (sortData?.field === key) {
$: if (index !== -1) { sorting = sortData.asc !== false ? 1 : 2;
sorting = sortData![index].split(":", 2)[1] === "dsc" ? 2 : 1;
} else { } else {
sorting = 0; sorting = 0;
} }
function onClick(): void { function onClick(): void {
if (!sortData) { if (sorting === 2) {
sortData = [key]; sortUpdate(undefined);
} else if (sorting === 2) {
delete sortData[index];
} else if (index !== -1) {
sortData[index] = sortData[index].split(":", 1) + ":dsc";
} else { } else {
sortData.push(key); sortUpdate({ field: key, asc: sorting !== 1 });
} }
sortUpdate(sortData);
} }
</script> </script>
@ -36,9 +32,6 @@
{#if sorting > 0} {#if sorting > 0}
<Icon path={sorting === 1 ? mdiSortAscending : mdiSortDescending} size={1} /> <Icon path={sorting === 1 ? mdiSortAscending : mdiSortDescending} size={1} />
{/if} {/if}
{#if sortData && sortData.length > 1 && index !== -1}
({index + 1})
{/if}
{title} {title}
</button> </button>
</th> </th>

View file

@ -1,11 +0,0 @@
<div class="join">
{#each { length: 4 } as _, i}
<button name="todo" class="join-item btn btn-sm" type="submit" value={i}>
{#if i === 0}
Notiz
{:else}
+{i}T
{/if}
</button>
{/each}
</div>

View file

@ -1,15 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Carta } from "@thetadev/carta-md"; import { Carta, Markdown } from "carta-md";
import { CARTA_CFG } from "./carta"; import { CARTA_CFG } from "./carta";
export let src: string; export let src: string;
const carta = new Carta(CARTA_CFG); const carta = new Carta(CARTA_CFG);
$: rendered = carta.renderSSR(src);
</script> </script>
<div class="carta-viewer carta-theme__default prose"> <Markdown {carta} value={src} />
<!-- eslint-disable-next-line svelte/no-at-html-tags HTML is sanitized -->
{@html rendered}
</div>

View file

@ -1,13 +1,11 @@
<script lang="ts"> <script lang="ts">
import "./carta.pcss"; import "./carta.pcss";
import { Carta, MarkdownEditor, type Labels } from "@thetadev/carta-md"; import { Carta, MarkdownEditor, type Labels } from "carta-md";
import type { InputConstraint } from "sveltekit-superforms"; import type { InputConstraint } from "sveltekit-superforms";
import { CARTA_CFG } from "./carta"; import { CARTA_CFG } from "./carta";
import "@thetadev/carta-md/highlight.css";
export let label = ""; export let label = "";
export let name = ""; export let name = "";
export let value = ""; export let value = "";

View file

@ -123,7 +123,7 @@
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.carta-highlight, .carta-highlight span { .shiki, .shiki span {
color: var(--hl-dark) !important; color: var(--shiki-dark) !important;
} }
} }

View file

@ -1,4 +1,4 @@
import type { Options } from "@thetadev/carta-md"; import type { Options } from "carta-md";
import { sanitizeHtml } from "$lib/shared/util"; import { sanitizeHtml } from "$lib/shared/util";

View file

@ -8,16 +8,15 @@ import type {
EntryVersionNew, EntryVersionNew,
Pagination, Pagination,
PaginationRequest, PaginationRequest,
SortRequest,
} from "$lib/shared/model"; } from "$lib/shared/model";
import { DateRange, normalizeLineEndings, utcDateToYMD } from "$lib/shared/util"; import { DateRange, normalizeLineEndings, utcDateToYMD } from "$lib/shared/util";
import { ErrorConflict } 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";
import { mapEntry, mapVersion, mapExecution } from "./mapping"; import { mapEntry, mapVersion, mapExecution } from "./mapping";
import { import { QueryBuilder, filterListToArray, parseSearchQuery } from "./util";
QueryBuilder, filterListToArray, mapSortFields, parseSearchQuery,
} from "./util";
const USER_SELECT = { select: { id: true, name: true } }; const USER_SELECT = { select: { id: true, name: true } };
@ -163,7 +162,7 @@ export async function newEntryExecution(
} }
// Check if there are any updates // Check if there are any updates
if (execution.text === cex?.text && execution.done === cex?.done) { if (execution.text === cex?.text) {
return cex.id; return cex.id;
} }
@ -172,7 +171,6 @@ export async function newEntryExecution(
entry_id, entry_id,
author_id, author_id,
text: execution.text, text: execution.text,
done: execution.done,
}, },
select: { id: true }, select: { id: true },
}); });
@ -183,7 +181,7 @@ export async function newEntryExecution(
export async function getEntries( export async function getEntries(
filter: EntriesFilter = {}, filter: EntriesFilter = {},
pagination: PaginationRequest = {}, pagination: PaginationRequest = {},
sort: string[] = [], sort: SortRequest = { field: "created_at", asc: false },
): Promise<Pagination<Entry>> { ): Promise<Pagination<Entry>> {
const qb = new QueryBuilder( const qb = new QueryBuilder(
`select `select
@ -201,7 +199,6 @@ export async function getEntries(
c.color as category_color, c.color as category_color,
ex.id as execution_id, ex.id as execution_id,
ex.text as execution_text, ex.text as execution_text,
ex.done as execution_done,
ex.created_at as execution_created_at, ex.created_at as execution_created_at,
xau.id as execution_author_id, xau.id as execution_author_id,
xau.name as execution_author_name, xau.name as execution_author_name,
@ -257,9 +254,9 @@ left join stations s on s.id = r.station_id`,
} }
if (filter?.done === true) { if (filter?.done === true) {
qb.addFilterClause("ex.done"); qb.addFilterClause("ex.id is not null");
} else if (filter?.done === false) { } else if (filter?.done === false) {
qb.addFilterClause("(ex.id is null or not ex.done)"); qb.addFilterClause("ex.id is null");
} }
qb.addFilterList("xau.id", filter?.executor); qb.addFilterList("xau.id", filter?.executor);
@ -302,11 +299,13 @@ left join stations s on s.id = r.station_id`,
date: ["ev.date"], date: ["ev.date"],
author: ["vau.name"], author: ["vau.name"],
executor: ["xau.name"], executor: ["xau.name"],
priority: ["ev.priority"],
}; };
const sortFields = mapSortFields(sort, "created_at", SORT_FIELDS); const sortFields = SORT_FIELDS[sort.field ?? "updated_at"];
qb.orderByFields(sortFields); if (!sortFields) {
throw new ErrorInvalidInput(`cannot sort by "${sort.field}"`);
}
qb.orderByFields(sortFields, sort.asc);
qb.setPagination(pagination); qb.setPagination(pagination);
type RowItem = { type RowItem = {
@ -324,7 +323,6 @@ left join stations s on s.id = r.station_id`,
category_color: string; category_color: string;
execution_id: number; execution_id: number;
execution_text: string; execution_text: string;
execution_done: boolean;
execution_created_at: Date; execution_created_at: Date;
execution_author_id: number; execution_author_id: number;
execution_author_name: string; execution_author_name: string;
@ -385,7 +383,6 @@ left join stations s on s.id = r.station_id`,
id: item.execution_id, id: item.execution_id,
author: { id: item.execution_author_id, name: item.execution_author_name }, author: { id: item.execution_author_id, name: item.execution_author_name },
text: item.execution_text, text: item.execution_text,
done: item.execution_done,
created_at: item.execution_created_at, created_at: item.execution_created_at,
} }
: null, : null,

View file

@ -82,7 +82,6 @@ export function mapExecution(execution: DbEntryExecutionLn): EntryExecution {
author: execution.author, author: execution.author,
created_at: execution.created_at, created_at: execution.created_at,
text: execution.text, text: execution.text,
done: execution.done,
}; };
} }

View file

@ -5,12 +5,14 @@ import type {
PaginationRequest, PaginationRequest,
PatientsFilter, PatientsFilter,
PatientTag, PatientTag,
SortRequest,
} from "$lib/shared/model"; } from "$lib/shared/model";
import { ErrorInvalidInput } from "$lib/shared/util/error";
import { prisma } from "$lib/server/prisma"; import { prisma } from "$lib/server/prisma";
import { mapPatient } from "./mapping"; import { mapPatient } from "./mapping";
import { QueryBuilder, handleDeleteConflict, mapSortFields } from "./util"; import { QueryBuilder, handleDeleteConflict } from "./util";
export async function newPatient(patient: PatientNew): Promise<number> { export async function newPatient(patient: PatientNew): Promise<number> {
const created = await prisma.patient.create({ data: patient, select: { id: true } }); const created = await prisma.patient.create({ data: patient, select: { id: true } });
@ -67,7 +69,7 @@ export async function getPatientNEntries(id: number): Promise<number> {
export async function getPatients( export async function getPatients(
filter: PatientsFilter = {}, filter: PatientsFilter = {},
pagination: PaginationRequest = {}, pagination: PaginationRequest = {},
sort: string[] = [], sort: SortRequest = { field: "created_at", asc: false },
): Promise<Pagination<Patient>> { ): Promise<Pagination<Patient>> {
const qb = new QueryBuilder( const qb = new QueryBuilder(
`select p.id, p.first_name, p.last_name, p.created_at, p.age, p.hidden, `select p.id, p.first_name, p.last_name, p.created_at, p.age, p.hidden,
@ -105,8 +107,11 @@ export async function getPatients(
created_at: ["p.created_at"], created_at: ["p.created_at"],
}; };
const sortFields = mapSortFields(sort, "created_at", SORT_FIELDS); const sortFields = SORT_FIELDS[sort.field ?? "created_at"];
qb.orderByFields(sortFields); if (!sortFields) {
throw new ErrorInvalidInput(`cannot sort by "${sort.field}"`);
}
qb.orderByFields(sortFields, sort.asc);
qb.setPagination(pagination); qb.setPagination(pagination);
type RowItem = { type RowItem = {

View file

@ -1,6 +1,6 @@
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { QueryBuilder, mapSortFields, parseSearchQuery } from "./util"; import { QueryBuilder, parseSearchQuery } from "./util";
test("query builder", () => { test("query builder", () => {
const qb = new QueryBuilder("select e.id, e.text, e.category", "from entries e"); const qb = new QueryBuilder("select e.id, e.text, e.category", "from entries e");
@ -56,19 +56,3 @@ test("parse search query", () => {
"'hello' <-> 'world' & 'banana' & !'cherry' & !('b' <-> 'x' <-> 'y') & 'vis':*", "'hello' <-> 'world' & 'banana' & !'cherry' & !('b' <-> 'x' <-> 'y') & 'vis':*",
); );
}); });
test("mapSortFields", () => {
const SORT_FIELDS: Record<string, string[]> = {
id: ["e.id"],
patient: ["p.last_name", "p.first_name"],
priority: ["ev.priority"],
};
const fields = ["id", "patient", "priority:dsc"];
const res = mapSortFields(fields, "", SORT_FIELDS);
expect(res).toStrictEqual([
{ name: "e.id", asc: true },
{ name: "p.last_name", asc: true },
{ name: "p.first_name", asc: true },
{ name: "ev.priority", asc: false },
]);
});

View file

@ -2,7 +2,7 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { PAGINATION_LIMIT } from "$lib/shared/constants"; import { PAGINATION_LIMIT } from "$lib/shared/constants";
import type { FilterList, PaginationRequest } from "$lib/shared/model"; import type { FilterList, PaginationRequest } from "$lib/shared/model";
import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error"; import { ErrorConflict } from "$lib/shared/util/error";
enum QueryComponentType { enum QueryComponentType {
Normal = 1, Normal = 1,
@ -37,34 +37,6 @@ export async function handleDeleteConflict(act: Promise<unknown>, msg: string):
} }
} }
export type SortField = {
name: string,
asc: boolean,
};
/** Map sort fields from request to a list of database columns for the query builder */
export function mapSortFields(
fields: string[],
defaultFilter: string,
mapping: Record<string, string[]>,
): SortField[] {
if (!fields || fields.length === 0) fields = [defaultFilter];
return fields.flatMap((sf) => {
const [fname, fdir] = sf.split(":", 2);
const mapped = mapping[fname];
if (!mapped) {
throw new ErrorInvalidInput(`cannot sort by "${sf}"`);
}
let asc = true;
if (fdir) {
if (fdir === "dsc") asc = false;
else if (fdir !== "asc") throw new ErrorInvalidInput("Direction must be asc/dsc");
}
return mapped.map((name) => ({ name, asc }));
});
}
class SearchQueryComponent { class SearchQueryComponent {
word: string; word: string;
@ -185,12 +157,10 @@ export class QueryBuilder {
this.orderClauses.push(orderClause); this.orderClauses.push(orderClause);
} }
orderByFields(fields: SortField[]): void { orderByFields(fields: string[], asc: boolean | undefined = undefined): void {
const clauseParts = fields.map((f) => { const sortDir = asc === false ? " desc" : " asc";
const sortDir = f.asc ? " asc" : " desc"; const orderClause = fields.join(`${sortDir}, `) + sortDir;
return f.name + sortDir; this.addOrderClause(orderClause);
});
this.addOrderClause(clauseParts.join(", "));
} }
/** Get the next parameter variable (e.g. $1) and increment the counter */ /** Get the next parameter variable (e.g. $1) and increment the counter */

View file

@ -39,7 +39,7 @@ export const entryRouter = t.router({
.query(async (opts) => trpcWrap(async () => getEntries( .query(async (opts) => trpcWrap(async () => getEntries(
opts.input.filter ?? {}, opts.input.filter ?? {},
opts.input.pagination ?? {}, opts.input.pagination ?? {},
opts.input.sort ?? [], opts.input.sort ?? {},
))), ))),
versions: t.procedure versions: t.procedure
.input(ZEntityId) .input(ZEntityId)

View file

@ -31,7 +31,7 @@ export const patientRouter = t.router({
.query(async (opts) => getPatients( .query(async (opts) => getPatients(
opts.input.filter ?? {}, opts.input.filter ?? {},
opts.input.pagination ?? {}, opts.input.pagination ?? {},
opts.input.sort ?? [], opts.input.sort ?? {},
)), )),
create: t.procedure create: t.procedure
.input(ZPatientNew) .input(ZPatientNew)

View file

@ -130,13 +130,11 @@ export type EntryExecution = {
id: number; id: number;
author: UserTag; author: UserTag;
text: string; text: string;
done: boolean;
created_at: Date; created_at: Date;
}; };
export type EntryExecutionNew = { export type EntryExecutionNew = {
text: string; text: string;
done: boolean;
}; };
export type SavedFilter = { export type SavedFilter = {

View file

@ -1,7 +1,7 @@
export type EntityQuery = Partial<{ export type EntityQuery = Partial<{
filter: EntriesFilter | PatientsFilter; filter: EntriesFilter | PatientsFilter;
pagination: PaginationRequest; pagination: PaginationRequest;
sort: string[]; sort: SortRequest;
}>; }>;
export type PaginationRequest = Partial<{ export type PaginationRequest = Partial<{
@ -9,6 +9,11 @@ export type PaginationRequest = Partial<{
offset: number | undefined; offset: number | undefined;
}>; }>;
export type SortRequest = Partial<{
field: string;
asc: boolean;
}>;
export type FilterList<T> = T | T[] | { id: T; name?: string }[]; export type FilterList<T> = T | T[] | { id: T; name?: string }[];
export type EntriesFilter = Partial<{ export type EntriesFilter = Partial<{

View file

@ -106,7 +106,7 @@ export const ZEntryVersionNew = implement<EntryVersionNew>().with({
export const ZEntryNew = implement<EntryNew>().with({ export const ZEntryNew = implement<EntryNew>().with({
patient_id: fields.EntityId(), patient_id: fields.EntityId(),
version: implement<EntryVersionNdata>().with({ version: implement<EntryVersionNdata>().with({
text: z.string(), text: fields.TextString(),
date: fields.DateString(), date: fields.DateString(),
category_id: fields.EntityId().nullable(), category_id: fields.EntityId().nullable(),
priority: z.boolean(), priority: z.boolean(),
@ -114,8 +114,7 @@ export const ZEntryNew = implement<EntryNew>().with({
}); });
export const ZEntryExecutionNew = implement<EntryExecutionNew>().with({ export const ZEntryExecutionNew = implement<EntryExecutionNew>().with({
text: z.string(), text: fields.TextString(),
done: z.boolean(),
}); });
export const ZPagination = implement<PaginationRequest>().with({ export const ZPagination = implement<PaginationRequest>().with({
@ -123,6 +122,11 @@ export const ZPagination = implement<PaginationRequest>().with({
offset: coercedUint.optional(), offset: coercedUint.optional(),
}); });
export const ZSort = z.object({
field: z.string().optional(),
asc: coercedBool.optional(),
});
const ZFilterListEntry = z.object({ const ZFilterListEntry = z.object({
id: coercedUint, id: coercedUint,
name: fields.NameString().optional(), name: fields.NameString().optional(),
@ -132,7 +136,7 @@ const paginatedQuery = <T extends z.ZodTypeAny>(f: T) => z
.object({ .object({
filter: f, filter: f,
pagination: ZPagination, pagination: ZPagination,
sort: z.array(z.string()), sort: ZSort,
}) })
.partial(); .partial();

View file

@ -25,7 +25,6 @@ export type EntryExecutionChange = {
created_at: Date; created_at: Date;
text: Diff.Change[]; text: Diff.Change[];
done?: boolean;
}; };
function newOrUndef<T>(o: T, n: T): T | undefined { function newOrUndef<T>(o: T, n: T): T | undefined {
@ -84,7 +83,6 @@ export function executionsDiff(executions: EntryExecution[]): EntryExecutionChan
author: v.author, author: v.author,
created_at: v.created_at, created_at: v.created_at,
text, text,
done: v.done,
}); });
prev = v; prev = v;

View file

@ -16,7 +16,7 @@ it("getQueryUrl", () => {
search: "Hello World", search: "Hello World",
}, },
pagination: { limit: 10, offset: 20 }, pagination: { limit: 10, offset: 20 },
sort: ["room"], sort: { field: "room", asc: true },
}; };
const queryUrl = getQueryUrl(query, ""); const queryUrl = getQueryUrl(query, "");

View file

@ -5,14 +5,13 @@ import { TRPCClientError } from "@trpc/client";
import DOMPurify from "isomorphic-dompurify"; import DOMPurify from "isomorphic-dompurify";
import qs from "qs"; import qs from "qs";
import type { FormOptions } from "sveltekit-superforms"; import type { FormOptions } from "sveltekit-superforms";
import type { TRPCClientInit } from "trpc-sveltekit";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { DEFAULT_FILTER_NAME, URL_VISIT } from "$lib/shared/constants"; import { DEFAULT_FILTER_NAME, URL_VISIT } from "$lib/shared/constants";
import type { EntityQuery, SavedFilter } from "$lib/shared/model"; import type { EntityQuery, SavedFilter } from "$lib/shared/model";
import { trpc, type RouterOutput } from "$lib/shared/trpc"; import { type RouterOutput } from "$lib/shared/trpc";
import { DateRange, utcDateToYMD } from "./date"; import { DateRange } from "./date";
import { toastError, toastInfo } from "./toast"; import { toastError, toastInfo } from "./toast";
export function formatBool(val: boolean): string { export function formatBool(val: boolean): string {
@ -157,19 +156,3 @@ export function defaultVisitUrl(): string {
}, },
}, URL_VISIT); }, URL_VISIT);
} }
export async function moveEntryTodoDate(id: number, nTodoDays: number, init?: TRPCClientInit) {
if (nTodoDays > 0) {
const entry = await trpc(init).entry.get.query(id);
const newDate = new Date(entry.current_version.date);
newDate.setDate(newDate.getDate() + nTodoDays);
await trpc(init).entry.newVersion.mutate({
id,
old_version_id: entry.current_version.id,
version: {
date: utcDateToYMD(newDate),
},
});
}
}

View file

@ -5,35 +5,23 @@ import { superValidate, message } from "sveltekit-superforms";
import { ZUrlEntityId } from "$lib/shared/model/validation"; import { ZUrlEntityId } from "$lib/shared/model/validation";
import { trpc } from "$lib/shared/trpc"; import { trpc } from "$lib/shared/trpc";
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util"; import { loadWrap } from "$lib/shared/util";
import { SchemaNewExecution } from "./editExecution/schema"; import { SchemaEntryExecution } from "./schema";
export const actions: Actions = { export const actions: Actions = {
default: async (event) => loadWrap(async () => { default: async (event) => loadWrap(async () => {
const formData = await event.request.formData(); const form = await superValidate(event.request, SchemaEntryExecution);
const form = await superValidate(formData, SchemaNewExecution);
if (!form.valid) { if (!form.valid) {
return fail(400, { form }); return fail(400, { form });
} }
const id = ZUrlEntityId.parse(event.params.id); const id = ZUrlEntityId.parse(event.params.id);
const todoDays = formData.get("todo");
const done = todoDays === null;
const nTodoDays = todoDays ? parseInt(todoDays.toString()) : 0;
await loadWrap(async () => {
await trpc(event).entry.newExecution.mutate({ await trpc(event).entry.newExecution.mutate({
id, id,
old_execution_id: form.data.old_execution_id, old_execution_id: null,
execution: { text: form.data.text, done: todoDays === null }, execution: { text: form.data.text },
}); });
await moveEntryTodoDate(id, nTodoDays, event); return message(form, "Eintrag erledigt");
});
if (nTodoDays > 0) {
return message(form, `Eintrag um ${nTodoDays} Tage in die Zukunft verschoben`);
}
return message(form, done ? "Eintrag erledigt" : "Eintrag mit Notiz versehen");
}), }),
}; };

View file

@ -2,29 +2,29 @@
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { superForm } from "sveltekit-superforms"; import { defaults, superForm } from "sveltekit-superforms";
import { superformConfig } from "$lib/shared/util"; import { superformConfig } from "$lib/shared/util";
import EntryBody from "$lib/components/entry/EntryBody.svelte"; import EntryBody from "$lib/components/entry/EntryBody.svelte";
import EntryTodoButton from "$lib/components/ui/EntryTodoButton.svelte";
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte"; import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
import { SchemaNewExecution } from "./editExecution/schema"; import { SchemaEntryExecution } from "./schema";
export let data: PageData; export let data: PageData;
const formData = defaults(SchemaEntryExecution);
const { const {
form, errors, enhance, form, errors, enhance,
} = superForm(data.form, { } = superForm(formData, {
validators: SchemaNewExecution, validators: SchemaEntryExecution,
...superformConfig("Eintrag"), ...superformConfig("Eintrag"),
}); });
</script> </script>
<EntryBody entry={data.entry} withExecution /> <EntryBody entry={data.entry} withExecution />
{#if !data.entry.execution?.done} {#if !data.entry.execution}
<form method="POST" use:enhance> <form method="POST" use:enhance>
<MarkdownInput <MarkdownInput
name="text" name="text"
@ -33,11 +33,9 @@
label="Eintrag erledigen" label="Eintrag erledigen"
bind:value={$form.text} bind:value={$form.text}
> >
<div class="row c-vlight gap-2"> <div class="row c-vlight">
<button class="btn btn-sm btn-primary" type="submit">Erledigt</button> <button class="btn btn-sm btn-primary" type="submit">Erledigt</button>
<EntryTodoButton />
</div> </div>
</MarkdownInput> </MarkdownInput>
<input name="old_execution_id" type="hidden" bind:value={$form.old_execution_id} />
</form> </form>
{/if} {/if}

View file

@ -1,26 +1,14 @@
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
import { superValidate } from "sveltekit-superforms";
import { ZUrlEntityId } from "$lib/shared/model/validation"; import { ZUrlEntityId } from "$lib/shared/model/validation";
import { trpc } from "$lib/shared/trpc"; import { trpc } from "$lib/shared/trpc";
import { loadWrap } from "$lib/shared/util"; import { loadWrap } from "$lib/shared/util";
import { SchemaNewExecution } from "./editExecution/schema";
export const load: PageLoad = async (event) => { export const load: PageLoad = async (event) => {
const entry = await loadWrap(async () => { const entry = await loadWrap(async () => {
const id = ZUrlEntityId.parse(event.params.id); const id = ZUrlEntityId.parse(event.params.id);
return trpc(event).entry.get.query(id); return trpc(event).entry.get.query(id);
}); });
const form = await superValidate( return { entry };
{
old_execution_id: entry.execution?.id,
...entry.execution,
},
SchemaNewExecution,
);
return { entry, form };
}; };

View file

@ -5,33 +5,24 @@ import { superValidate } from "sveltekit-superforms";
import { ZUrlEntityId } from "$lib/shared/model/validation"; import { ZUrlEntityId } from "$lib/shared/model/validation";
import { trpc } from "$lib/shared/trpc"; import { trpc } from "$lib/shared/trpc";
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util"; import { loadWrap } from "$lib/shared/util";
import { SchemaNewExecution } from "./schema"; import { SchemaNewExecution } from "./schema";
export const actions: Actions = { export const actions: Actions = {
default: async (event) => loadWrap(async () => { default: async (event) => loadWrap(async () => {
const formData = await event.request.formData(); const form = await superValidate(event.request, SchemaNewExecution);
const form = await superValidate(formData, SchemaNewExecution);
if (!form.valid) { if (!form.valid) {
return fail(400, { form }); return fail(400, { form });
} }
const id = ZUrlEntityId.parse(event.params.id); const id = ZUrlEntityId.parse(event.params.id);
const todoDays = formData.get("todo");
await trpc(event).entry.newExecution.mutate({ await trpc(event).entry.newExecution.mutate({
id, id,
execution: { execution: form.data,
text: form.data.text,
done: todoDays === null,
},
old_execution_id: form.data.old_execution_id, old_execution_id: form.data.old_execution_id,
}); });
const nTodoDays = todoDays ? parseInt(todoDays.toString()) : 0;
await moveEntryTodoDate(id, nTodoDays, event);
redirect(302, `/entry/${id}`); redirect(302, `/entry/${id}`);
}), }),
}; };

View file

@ -2,19 +2,19 @@
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { superForm } from "sveltekit-superforms"; import { defaults, superForm } from "sveltekit-superforms";
import { superformConfig } from "$lib/shared/util"; import { superformConfig } from "$lib/shared/util";
import EntryBody from "$lib/components/entry/EntryBody.svelte"; import EntryBody from "$lib/components/entry/EntryBody.svelte";
import EntryTodoButton from "$lib/components/ui/EntryTodoButton.svelte";
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte"; import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
import { SchemaNewExecution } from "./schema"; import { SchemaNewExecution } from "./schema";
export let data: PageData; export let data: PageData;
const { form, errors, enhance } = superForm(data.form, { const formData = defaults(SchemaNewExecution);
const { form, errors, enhance } = superForm(formData, {
validators: SchemaNewExecution, validators: SchemaNewExecution,
...superformConfig("Eintrag"), ...superformConfig("Eintrag"),
}); });
@ -30,10 +30,8 @@
label="Ausführungstext bearbeiten" label="Ausführungstext bearbeiten"
bind:value={$form.text} bind:value={$form.text}
> >
<div class="row c-vlight gap-2"> <div class="row c-vlight">
<button class="btn btn-sm btn-primary" type="submit">Speichern</button> <button class="btn btn-sm btn-primary" type="submit">Speichern</button>
<EntryTodoButton />
</div> </div>
</MarkdownInput> </MarkdownInput>
<input name="old_execution_id" type="hidden" bind:value={$form.old_execution_id} />
</form> </form>

View file

@ -22,9 +22,6 @@
<div class="row c-light text-sm"> <div class="row c-light text-sm">
#{i + 1}&nbsp; #{i + 1}&nbsp;
<UserField user={version.author} />, {formatDate(version.created_at, true)} <UserField user={version.author} />, {formatDate(version.created_at, true)}
{#if !version.done}
<div class="badge ellipsis badge-warning ml-2">Notiz</div>
{/if}
</div> </div>
{#if version.text.length > 0} {#if version.text.length > 0}
<div class="rowb whitespace-pre-wrap"> <div class="rowb whitespace-pre-wrap">

View file

@ -22,7 +22,7 @@ export const load: PageLoad = async (event) => {
// Sort entries by date // Sort entries by date
if (!query.sort) { if (!query.sort) {
query.sort = ["priority:dsc", "date"]; query.sort = { field: "date" };
} }
const entries = await trpc(event).entry.list.query(query); const entries = await trpc(event).entry.list.query(query);

View file

@ -57,8 +57,8 @@ async function insertTestEntries() {
}); });
// Execute entries // Execute entries
await newEntryExecution(1, eId2, { text: "Some execution txt", done: true }); await newEntryExecution(1, eId2, { text: "Some execution txt" });
await newEntryExecution(2, eId3, { text: "More execution txt", done: true }); await newEntryExecution(2, eId3, { text: "More execution txt" });
return { eId1, eId2, eId3 }; return { eId1, eId2, eId3 };
} }
@ -166,7 +166,7 @@ test("create entry execution", async () => {
}); });
const text = "Blutabnahme erledigt."; const text = "Blutabnahme erledigt.";
const xId = await newEntryExecution(1, eId, { text, done: true }, null); const xId = await newEntryExecution(1, eId, { text }, null);
const entry = await getEntry(eId); const entry = await getEntry(eId);
expect(entry.execution?.id).toBe(xId); expect(entry.execution?.id).toBe(xId);
@ -183,8 +183,8 @@ test("create entry execution (update)", async () => {
version: TEST_VERSION, version: TEST_VERSION,
}); });
const x1 = await newEntryExecution(1, eId, { text: "x1", done: true }, null); const x1 = await newEntryExecution(1, eId, { text: "x1" }, null);
const x2 = await newEntryExecution(2, eId, { text: "x2", done: true }, x1); const x2 = await newEntryExecution(2, eId, { text: "x2" }, x1);
const entry = await getEntry(eId); const entry = await getEntry(eId);
expect(entry.execution?.id).toBe(x2); expect(entry.execution?.id).toBe(x2);
@ -200,9 +200,9 @@ test("create entry execution (wrong old xid)", async () => {
patient_id: 1, patient_id: 1,
version: TEST_VERSION, version: TEST_VERSION,
}); });
const x1 = await newEntryExecution(1, eId, { text: "x1", done: true }, null); const x1 = await newEntryExecution(1, eId, { text: "x1" }, null);
expect(async () => newEntryExecution(1, eId, { text: "x2", done: true }, x1 + 1)).rejects.toThrowError(new ErrorConflict("old execution id does not match")); expect(async () => newEntryExecution(1, eId, { text: "x2" }, x1 + 1)).rejects.toThrowError(new ErrorConflict("old execution id does not match"));
}); });
test("get entries", async () => { test("get entries", async () => {
@ -246,8 +246,8 @@ test("get entries", async () => {
const entriesPatient = await getEntries({ patient: 1 }, {}); const entriesPatient = await getEntries({ patient: 1 }, {});
expect(entriesPatient.items).length(2); expect(entriesPatient.items).length(2);
expect(entriesPatient.total).toBe(2); expect(entriesPatient.total).toBe(2);
expect(entriesPatient.items[0].id).toBe(eId1); expect(entriesPatient.items[0].id).toBe(eId3);
expect(entriesPatient.items[1].id).toBe(eId3); expect(entriesPatient.items[1].id).toBe(eId1);
// Filter by room // Filter by room
const entriesRoom = await getEntries({ room: 1 }, {}); const entriesRoom = await getEntries({ room: 1 }, {});
@ -257,8 +257,8 @@ test("get entries", async () => {
const entriesDone = await getEntries({ done: true }, {}); const entriesDone = await getEntries({ done: true }, {});
expect(entriesDone.items).length(2); expect(entriesDone.items).length(2);
expect(entriesDone.total).toBe(2); expect(entriesDone.total).toBe(2);
expect(entriesDone.items[0].id).toBe(eId2); expect(entriesDone.items[0].id).toBe(eId3);
expect(entriesDone.items[1].id).toBe(eId3); expect(entriesDone.items[1].id).toBe(eId2);
// Filter not done // Filter not done
const entriesNotDone = await getEntries({ done: false }, {}); const entriesNotDone = await getEntries({ done: false }, {});
@ -281,8 +281,8 @@ test("get entries", async () => {
const entriesDateRange = await getEntries({ date: "2024-01-05..2024-01-06" }); const entriesDateRange = await getEntries({ date: "2024-01-05..2024-01-06" });
expect(entriesDateRange.items).length(2); expect(entriesDateRange.items).length(2);
expect(entriesDateRange.total).toBe(2); expect(entriesDateRange.total).toBe(2);
expect(entriesDateRange.items[0].id).toBe(eId2); expect(entriesDateRange.items[0].id).toBe(eId3);
expect(entriesDateRange.items[1].id).toBe(eId3); expect(entriesDateRange.items[1].id).toBe(eId2);
// Search // Search
const entriesSearch = await getEntries({ search: "Blu" }, {}); const entriesSearch = await getEntries({ search: "Blu" }, {});
@ -293,30 +293,6 @@ test("get entries", async () => {
// NTodo // NTodo
const n = await getNTodo(new Date("2024-01-05")); const n = await getNTodo(new Date("2024-01-05"));
expect(n).toBe(1); expect(n).toBe(1);
// Sort by ID
const entriesSortedId = await getEntries({}, {}, ["id"]);
const entriesSortedIdDsc = await getEntries({}, {}, ["id:dsc"]);
expect(entriesSortedId.total).toBe(3);
expect(entriesSortedIdDsc.total).toBe(3);
for (let i = 0; i < 3; i++) {
expect(entriesSortedId.items[i]).toStrictEqual(
entriesSortedIdDsc.items[entriesSortedIdDsc.items.length - i - 1],
);
}
// Sort by patient and ID
const entriesSortedPatientId = await getEntries({}, {}, ["patient", "id"]);
expect(entriesSortedPatientId.items.length).toBe(3);
expect(entriesSortedPatientId.items[0].id).toBe(eId1);
expect(entriesSortedPatientId.items[1].id).toBe(eId3);
expect(entriesSortedPatientId.items[2].id).toBe(eId2);
const entriesSortedPatientIdDsc = await getEntries({}, {}, ["patient:dsc", "id"]);
expect(entriesSortedPatientIdDsc.items.length).toBe(3);
expect(entriesSortedPatientIdDsc.items[0].id).toBe(eId2);
expect(entriesSortedPatientIdDsc.items[1].id).toBe(eId1);
expect(entriesSortedPatientIdDsc.items[2].id).toBe(eId3);
}); });
test("get patient n entries", async () => { test("get patient n entries", async () => {
@ -328,18 +304,3 @@ test("get category n entries", async () => {
await insertTestEntries(); await insertTestEntries();
expect(await getCategoryNEntries(3)).toBe(1); expect(await getCategoryNEntries(3)).toBe(1);
}); });
test("todo execution", async () => {
const eId = await newEntry(1, {
patient_id: 1,
version: TEST_VERSION,
});
const n1 = await newEntryExecution(1, eId, { text: "note1", done: false }, null);
const entry = await getEntry(eId);
expect(entry.execution?.id).toBe(n1);
expect(entry.execution?.text).toBe("note1");
expect(entry.execution?.author.id).toBe(1);
expect(entry.execution?.done).toBe(false);
});

View file

@ -11,7 +11,7 @@ import {
} from "$lib/server/query"; } from "$lib/server/query";
import { S1, S2 } from "$tests/helpers/testdata"; import { S1, S2 } from "$tests/helpers/testdata";
const SORT_ID = ["id"]; const SORT_ID = { field: "id" };
test("create patient", async () => { test("create patient", async () => {
const pId = await newPatient({ const pId = await newPatient({