Compare commits
4 commits
c2c21d1296
...
6c338e447e
Author | SHA1 | Date | |
---|---|---|---|
6c338e447e | |||
2a7aa618d9 | |||
f630ee08b8 | |||
f485db2968 |
39 changed files with 324 additions and 185 deletions
|
@ -3,6 +3,14 @@
|
|||
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
|
||||
|
||||
### 🚀 Features
|
||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "visitenbuch",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
@ -21,7 +21,7 @@
|
|||
"@floating-ui/core": "^1.6.1",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@prisma/client": "^5.13.0",
|
||||
"carta-md": "4.0.2",
|
||||
"@thetadev/carta-md": "^1.0.1",
|
||||
"diff": "^5.2.0",
|
||||
"isomorphic-dompurify": "^2.9.0",
|
||||
"prisma": "^5.13.0",
|
||||
|
@ -71,10 +71,5 @@
|
|||
"vite": "^5.2.11",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"type": "module",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"carta-md@4.0.2": "patches/carta-md@4.0.2.patch"
|
||||
}
|
||||
}
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
# 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}
|
|
@ -4,11 +4,6 @@ settings:
|
|||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
carta-md@4.0.2:
|
||||
hash: i33ea43vfgrg3ziu25cfu7s2zq
|
||||
path: patches/carta-md@4.0.2.patch
|
||||
|
||||
dependencies:
|
||||
'@auth/core':
|
||||
specifier: ^0.30.0
|
||||
|
@ -22,9 +17,9 @@ dependencies:
|
|||
'@prisma/client':
|
||||
specifier: ^5.13.0
|
||||
version: 5.13.0(prisma@5.13.0)
|
||||
carta-md:
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2(patch_hash=i33ea43vfgrg3ziu25cfu7s2zq)(svelte@4.2.15)
|
||||
'@thetadev/carta-md':
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1(svelte@4.2.15)
|
||||
diff:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
|
@ -889,10 +884,6 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/@shikijs/core@1.4.0:
|
||||
resolution: {integrity: sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==}
|
||||
dev: false
|
||||
|
||||
/@sideway/address@4.1.5:
|
||||
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
|
||||
requiresBuild: true
|
||||
|
@ -1088,6 +1079,22 @@ packages:
|
|||
tailwindcss: 3.4.3
|
||||
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):
|
||||
resolution: {integrity: sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg==}
|
||||
peerDependencies:
|
||||
|
@ -1757,23 +1764,6 @@ packages:
|
|||
resolution: {integrity: sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
dev: false
|
||||
|
@ -4424,6 +4414,11 @@ packages:
|
|||
'@prisma/engines': 5.13.0
|
||||
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:
|
||||
resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==}
|
||||
requiresBuild: true
|
||||
|
@ -4721,12 +4716,6 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
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:
|
||||
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "entry_executions" ADD COLUMN "done" BOOLEAN NOT NULL DEFAULT true;
|
|
@ -142,6 +142,7 @@ model EntryExecution {
|
|||
entry_id Int
|
||||
|
||||
text String
|
||||
done Boolean @default(true)
|
||||
|
||||
author User @relation(fields: [author_id], references: [id])
|
||||
author_id Int
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
<p class="text-sm flex gap-2">
|
||||
<span>Erstellt {humanDate(entry.created_at, true)}</span>
|
||||
<span>·</span>
|
||||
{#if entry.execution}
|
||||
{#if entry.execution?.done}
|
||||
<span>Erledigt {humanDate(entry.execution.created_at)}</span>
|
||||
{:else}
|
||||
<span>Zu erledigen {humanDate(entry.current_version.date)}</span>
|
||||
|
@ -53,7 +53,7 @@
|
|||
Beschreibung
|
||||
<div>
|
||||
<VersionsButton href="{basePath}/versions" n={entry.n_versions} />
|
||||
<a class="btn btn-circle btn-sm btn-ghost" href="{basePath}/edit">
|
||||
<a class="btn btn-circle btn-xs btn-ghost" href="{basePath}/edit">
|
||||
<Icon path={mdiPencil} size={1.2} />
|
||||
</a>
|
||||
</div>
|
||||
|
@ -69,9 +69,12 @@
|
|||
|
||||
{#if withExecution && entry.execution}
|
||||
<div class="card2">
|
||||
<div class="row c-light text-sm items-center justify-between">
|
||||
<div
|
||||
class="row c-light text-sm items-center justify-between"
|
||||
class:done={entry.execution.done}
|
||||
class:note={!entry.execution.done}>
|
||||
<p>
|
||||
Erledigt am {formatDate(entry.execution.created_at, true)} von
|
||||
{entry.execution.done ? "Erledigt am" : "Notiz von"} {formatDate(entry.execution.created_at, true)} von
|
||||
<UserField filterName="executor" user={entry.execution.author} />
|
||||
</p>
|
||||
<div>
|
||||
|
@ -88,3 +91,12 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.done {
|
||||
@apply bg-success/20;
|
||||
}
|
||||
.note {
|
||||
@apply bg-warning/20;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import type { SortRequest } from "$lib/shared/model";
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
import { formatDate } from "$lib/shared/util";
|
||||
|
||||
|
@ -10,8 +9,8 @@
|
|||
import UserField from "./UserField.svelte";
|
||||
|
||||
export let entries: RouterOutput["entry"]["list"];
|
||||
export let sortData: SortRequest | undefined;
|
||||
export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
|
||||
export let sortData: string[] | undefined;
|
||||
export let sortUpdate: (sort: string[] | undefined) => void = () => {};
|
||||
export let perPatient = false;
|
||||
export let baseUrl: string;
|
||||
</script>
|
||||
|
@ -38,7 +37,7 @@
|
|||
{#each entries.items as entry (entry.id)}
|
||||
<tr
|
||||
class="transition-colors hover:bg-neutral-content/10"
|
||||
class:done={entry.execution}
|
||||
class:done={entry.execution?.done}
|
||||
class:priority={entry.current_version.priority}
|
||||
>
|
||||
<td
|
||||
|
@ -67,7 +66,7 @@
|
|||
<td><UserField {baseUrl} user={entry.current_version.author} /></td>
|
||||
<td><span class="line-clamp-2">{entry.current_version.text}</span></td>
|
||||
<td>
|
||||
{#if entry.execution}
|
||||
{#if entry.execution?.done}
|
||||
{formatDate(entry.execution.created_at, true)}
|
||||
<UserField
|
||||
{baseUrl}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { z } from "zod";
|
||||
|
||||
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||
import type { PaginationRequest } from "$lib/shared/model";
|
||||
import type { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||
import { type RouterOutput } from "$lib/shared/trpc";
|
||||
import { getQueryUrl } from "$lib/shared/util";
|
||||
|
@ -34,7 +34,7 @@
|
|||
updateQuery({ filter, sort: query.sort });
|
||||
}
|
||||
|
||||
function sortUpdate(sort: SortRequest | undefined): void {
|
||||
function sortUpdate(sort: string[] | undefined): void {
|
||||
updateQuery({
|
||||
filter: query.filter,
|
||||
pagination: query.pagination,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { z } from "zod";
|
||||
|
||||
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||
import type { PaginationRequest } from "$lib/shared/model";
|
||||
import type { ZPatientsQuery } from "$lib/shared/model/validation";
|
||||
import { type RouterOutput } from "$lib/shared/trpc";
|
||||
import { getQueryUrl } from "$lib/shared/util";
|
||||
|
@ -32,7 +32,7 @@
|
|||
updateQuery({ filter, sort: query.sort });
|
||||
}
|
||||
|
||||
function sortUpdate(sort: SortRequest | undefined): void {
|
||||
function sortUpdate(sort: string[] | undefined): void {
|
||||
updateQuery({
|
||||
filter: query.filter,
|
||||
pagination: query.pagination,
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { mdiFilter } from "@mdi/js";
|
||||
|
||||
import { URL_ENTRIES } from "$lib/shared/constants";
|
||||
import type { SortRequest } from "$lib/shared/model";
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
import { formatDate, gotoEntityQuery } from "$lib/shared/util";
|
||||
|
||||
|
@ -12,8 +11,8 @@
|
|||
import SortHeader from "./SortHeader.svelte";
|
||||
|
||||
export let patients: RouterOutput["patient"]["list"];
|
||||
export let sortData: SortRequest | undefined;
|
||||
export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
|
||||
export let sortData: string[] | undefined;
|
||||
export let sortUpdate: (sort: string[] | undefined) => void = () => {};
|
||||
export let baseUrl: string;
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,29 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { mdiSortAscending, mdiSortDescending } from "@mdi/js";
|
||||
|
||||
import type { SortRequest } from "$lib/shared/model";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
|
||||
export let key: string;
|
||||
export let title: string;
|
||||
export let sortData: SortRequest | undefined;
|
||||
export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
|
||||
export let sortData: string[] | undefined;
|
||||
export let sortUpdate: (sort: string[] | undefined) => void = () => {};
|
||||
|
||||
// 1: asc, 2: desc, 0: not sorted
|
||||
let sorting = 0;
|
||||
$: if (sortData?.field === key) {
|
||||
sorting = sortData.asc !== false ? 1 : 2;
|
||||
$: index = sortData?.findIndex((itm) => itm.split(":", 1)[0] === key) ?? -1;
|
||||
$: if (index !== -1) {
|
||||
sorting = sortData![index].split(":", 2)[1] === "dsc" ? 2 : 1;
|
||||
} else {
|
||||
sorting = 0;
|
||||
}
|
||||
|
||||
function onClick(): void {
|
||||
if (sorting === 2) {
|
||||
sortUpdate(undefined);
|
||||
if (!sortData) {
|
||||
sortData = [key];
|
||||
} else if (sorting === 2) {
|
||||
delete sortData[index];
|
||||
} else if (index !== -1) {
|
||||
sortData[index] = sortData[index].split(":", 1) + ":dsc";
|
||||
} else {
|
||||
sortUpdate({ field: key, asc: sorting !== 1 });
|
||||
sortData.push(key);
|
||||
}
|
||||
sortUpdate(sortData);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -32,6 +36,9 @@
|
|||
{#if sorting > 0}
|
||||
<Icon path={sorting === 1 ? mdiSortAscending : mdiSortDescending} size={1} />
|
||||
{/if}
|
||||
{#if sortData && sortData.length > 1 && index !== -1}
|
||||
({index + 1})
|
||||
{/if}
|
||||
{title}
|
||||
</button>
|
||||
</th>
|
||||
|
|
11
src/lib/components/ui/EntryTodoButton.svelte
Normal file
11
src/lib/components/ui/EntryTodoButton.svelte
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
|
@ -1,10 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { Carta, Markdown } from "carta-md";
|
||||
import { Carta } from "@thetadev/carta-md";
|
||||
|
||||
import { CARTA_CFG } from "./carta";
|
||||
|
||||
export let src: string;
|
||||
const carta = new Carta(CARTA_CFG);
|
||||
|
||||
$: rendered = carta.renderSSR(src);
|
||||
</script>
|
||||
|
||||
<Markdown {carta} value={src} />
|
||||
<div class="carta-viewer carta-theme__default prose">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags HTML is sanitized -->
|
||||
{@html rendered}
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts">
|
||||
import "./carta.pcss";
|
||||
|
||||
import { Carta, MarkdownEditor, type Labels } from "carta-md";
|
||||
import { Carta, MarkdownEditor, type Labels } from "@thetadev/carta-md";
|
||||
import type { InputConstraint } from "sveltekit-superforms";
|
||||
|
||||
import { CARTA_CFG } from "./carta";
|
||||
|
||||
import "@thetadev/carta-md/highlight.css";
|
||||
|
||||
export let label = "";
|
||||
export let name = "";
|
||||
export let value = "";
|
||||
|
|
|
@ -123,7 +123,7 @@
|
|||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.shiki, .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
.carta-highlight, .carta-highlight span {
|
||||
color: var(--hl-dark) !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Options } from "carta-md";
|
||||
import type { Options } from "@thetadev/carta-md";
|
||||
|
||||
import { sanitizeHtml } from "$lib/shared/util";
|
||||
|
||||
|
|
|
@ -8,15 +8,16 @@ import type {
|
|||
EntryVersionNew,
|
||||
Pagination,
|
||||
PaginationRequest,
|
||||
SortRequest,
|
||||
} from "$lib/shared/model";
|
||||
import { DateRange, normalizeLineEndings, utcDateToYMD } from "$lib/shared/util";
|
||||
import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error";
|
||||
import { ErrorConflict } from "$lib/shared/util/error";
|
||||
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
import { mapEntry, mapVersion, mapExecution } from "./mapping";
|
||||
import { QueryBuilder, filterListToArray, parseSearchQuery } from "./util";
|
||||
import {
|
||||
QueryBuilder, filterListToArray, mapSortFields, parseSearchQuery,
|
||||
} from "./util";
|
||||
|
||||
const USER_SELECT = { select: { id: true, name: true } };
|
||||
|
||||
|
@ -162,7 +163,7 @@ export async function newEntryExecution(
|
|||
}
|
||||
|
||||
// Check if there are any updates
|
||||
if (execution.text === cex?.text) {
|
||||
if (execution.text === cex?.text && execution.done === cex?.done) {
|
||||
return cex.id;
|
||||
}
|
||||
|
||||
|
@ -171,6 +172,7 @@ export async function newEntryExecution(
|
|||
entry_id,
|
||||
author_id,
|
||||
text: execution.text,
|
||||
done: execution.done,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
@ -181,7 +183,7 @@ export async function newEntryExecution(
|
|||
export async function getEntries(
|
||||
filter: EntriesFilter = {},
|
||||
pagination: PaginationRequest = {},
|
||||
sort: SortRequest = { field: "created_at", asc: false },
|
||||
sort: string[] = [],
|
||||
): Promise<Pagination<Entry>> {
|
||||
const qb = new QueryBuilder(
|
||||
`select
|
||||
|
@ -199,6 +201,7 @@ export async function getEntries(
|
|||
c.color as category_color,
|
||||
ex.id as execution_id,
|
||||
ex.text as execution_text,
|
||||
ex.done as execution_done,
|
||||
ex.created_at as execution_created_at,
|
||||
xau.id as execution_author_id,
|
||||
xau.name as execution_author_name,
|
||||
|
@ -254,9 +257,9 @@ left join stations s on s.id = r.station_id`,
|
|||
}
|
||||
|
||||
if (filter?.done === true) {
|
||||
qb.addFilterClause("ex.id is not null");
|
||||
qb.addFilterClause("ex.done");
|
||||
} else if (filter?.done === false) {
|
||||
qb.addFilterClause("ex.id is null");
|
||||
qb.addFilterClause("(ex.id is null or not ex.done)");
|
||||
}
|
||||
|
||||
qb.addFilterList("xau.id", filter?.executor);
|
||||
|
@ -299,13 +302,11 @@ left join stations s on s.id = r.station_id`,
|
|||
date: ["ev.date"],
|
||||
author: ["vau.name"],
|
||||
executor: ["xau.name"],
|
||||
priority: ["ev.priority"],
|
||||
};
|
||||
|
||||
const sortFields = SORT_FIELDS[sort.field ?? "updated_at"];
|
||||
if (!sortFields) {
|
||||
throw new ErrorInvalidInput(`cannot sort by "${sort.field}"`);
|
||||
}
|
||||
qb.orderByFields(sortFields, sort.asc);
|
||||
const sortFields = mapSortFields(sort, "created_at", SORT_FIELDS);
|
||||
qb.orderByFields(sortFields);
|
||||
qb.setPagination(pagination);
|
||||
|
||||
type RowItem = {
|
||||
|
@ -323,6 +324,7 @@ left join stations s on s.id = r.station_id`,
|
|||
category_color: string;
|
||||
execution_id: number;
|
||||
execution_text: string;
|
||||
execution_done: boolean;
|
||||
execution_created_at: Date;
|
||||
execution_author_id: number;
|
||||
execution_author_name: string;
|
||||
|
@ -383,6 +385,7 @@ left join stations s on s.id = r.station_id`,
|
|||
id: item.execution_id,
|
||||
author: { id: item.execution_author_id, name: item.execution_author_name },
|
||||
text: item.execution_text,
|
||||
done: item.execution_done,
|
||||
created_at: item.execution_created_at,
|
||||
}
|
||||
: null,
|
||||
|
|
|
@ -82,6 +82,7 @@ export function mapExecution(execution: DbEntryExecutionLn): EntryExecution {
|
|||
author: execution.author,
|
||||
created_at: execution.created_at,
|
||||
text: execution.text,
|
||||
done: execution.done,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,14 +5,12 @@ import type {
|
|||
PaginationRequest,
|
||||
PatientsFilter,
|
||||
PatientTag,
|
||||
SortRequest,
|
||||
} from "$lib/shared/model";
|
||||
import { ErrorInvalidInput } from "$lib/shared/util/error";
|
||||
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
import { mapPatient } from "./mapping";
|
||||
import { QueryBuilder, handleDeleteConflict } from "./util";
|
||||
import { QueryBuilder, handleDeleteConflict, mapSortFields } from "./util";
|
||||
|
||||
export async function newPatient(patient: PatientNew): Promise<number> {
|
||||
const created = await prisma.patient.create({ data: patient, select: { id: true } });
|
||||
|
@ -69,7 +67,7 @@ export async function getPatientNEntries(id: number): Promise<number> {
|
|||
export async function getPatients(
|
||||
filter: PatientsFilter = {},
|
||||
pagination: PaginationRequest = {},
|
||||
sort: SortRequest = { field: "created_at", asc: false },
|
||||
sort: string[] = [],
|
||||
): Promise<Pagination<Patient>> {
|
||||
const qb = new QueryBuilder(
|
||||
`select p.id, p.first_name, p.last_name, p.created_at, p.age, p.hidden,
|
||||
|
@ -107,11 +105,8 @@ export async function getPatients(
|
|||
created_at: ["p.created_at"],
|
||||
};
|
||||
|
||||
const sortFields = SORT_FIELDS[sort.field ?? "created_at"];
|
||||
if (!sortFields) {
|
||||
throw new ErrorInvalidInput(`cannot sort by "${sort.field}"`);
|
||||
}
|
||||
qb.orderByFields(sortFields, sort.asc);
|
||||
const sortFields = mapSortFields(sort, "created_at", SORT_FIELDS);
|
||||
qb.orderByFields(sortFields);
|
||||
qb.setPagination(pagination);
|
||||
|
||||
type RowItem = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, test } from "vitest";
|
||||
|
||||
import { QueryBuilder, parseSearchQuery } from "./util";
|
||||
import { QueryBuilder, mapSortFields, parseSearchQuery } from "./util";
|
||||
|
||||
test("query builder", () => {
|
||||
const qb = new QueryBuilder("select e.id, e.text, e.category", "from entries e");
|
||||
|
@ -56,3 +56,19 @@ test("parse search query", () => {
|
|||
"'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 },
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|||
|
||||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
||||
import type { FilterList, PaginationRequest } from "$lib/shared/model";
|
||||
import { ErrorConflict } from "$lib/shared/util/error";
|
||||
import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error";
|
||||
|
||||
enum QueryComponentType {
|
||||
Normal = 1,
|
||||
|
@ -37,6 +37,34 @@ 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 {
|
||||
word: string;
|
||||
|
||||
|
@ -157,10 +185,12 @@ export class QueryBuilder {
|
|||
this.orderClauses.push(orderClause);
|
||||
}
|
||||
|
||||
orderByFields(fields: string[], asc: boolean | undefined = undefined): void {
|
||||
const sortDir = asc === false ? " desc" : " asc";
|
||||
const orderClause = fields.join(`${sortDir}, `) + sortDir;
|
||||
this.addOrderClause(orderClause);
|
||||
orderByFields(fields: SortField[]): void {
|
||||
const clauseParts = fields.map((f) => {
|
||||
const sortDir = f.asc ? " asc" : " desc";
|
||||
return f.name + sortDir;
|
||||
});
|
||||
this.addOrderClause(clauseParts.join(", "));
|
||||
}
|
||||
|
||||
/** Get the next parameter variable (e.g. $1) and increment the counter */
|
||||
|
|
|
@ -39,7 +39,7 @@ export const entryRouter = t.router({
|
|||
.query(async (opts) => trpcWrap(async () => getEntries(
|
||||
opts.input.filter ?? {},
|
||||
opts.input.pagination ?? {},
|
||||
opts.input.sort ?? {},
|
||||
opts.input.sort ?? [],
|
||||
))),
|
||||
versions: t.procedure
|
||||
.input(ZEntityId)
|
||||
|
|
|
@ -31,7 +31,7 @@ export const patientRouter = t.router({
|
|||
.query(async (opts) => getPatients(
|
||||
opts.input.filter ?? {},
|
||||
opts.input.pagination ?? {},
|
||||
opts.input.sort ?? {},
|
||||
opts.input.sort ?? [],
|
||||
)),
|
||||
create: t.procedure
|
||||
.input(ZPatientNew)
|
||||
|
|
|
@ -130,11 +130,13 @@ export type EntryExecution = {
|
|||
id: number;
|
||||
author: UserTag;
|
||||
text: string;
|
||||
done: boolean;
|
||||
created_at: Date;
|
||||
};
|
||||
|
||||
export type EntryExecutionNew = {
|
||||
text: string;
|
||||
done: boolean;
|
||||
};
|
||||
|
||||
export type SavedFilter = {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export type EntityQuery = Partial<{
|
||||
filter: EntriesFilter | PatientsFilter;
|
||||
pagination: PaginationRequest;
|
||||
sort: SortRequest;
|
||||
sort: string[];
|
||||
}>;
|
||||
|
||||
export type PaginationRequest = Partial<{
|
||||
|
@ -9,11 +9,6 @@ export type PaginationRequest = Partial<{
|
|||
offset: number | undefined;
|
||||
}>;
|
||||
|
||||
export type SortRequest = Partial<{
|
||||
field: string;
|
||||
asc: boolean;
|
||||
}>;
|
||||
|
||||
export type FilterList<T> = T | T[] | { id: T; name?: string }[];
|
||||
|
||||
export type EntriesFilter = Partial<{
|
||||
|
|
|
@ -106,7 +106,7 @@ export const ZEntryVersionNew = implement<EntryVersionNew>().with({
|
|||
export const ZEntryNew = implement<EntryNew>().with({
|
||||
patient_id: fields.EntityId(),
|
||||
version: implement<EntryVersionNdata>().with({
|
||||
text: fields.TextString(),
|
||||
text: z.string(),
|
||||
date: fields.DateString(),
|
||||
category_id: fields.EntityId().nullable(),
|
||||
priority: z.boolean(),
|
||||
|
@ -114,7 +114,8 @@ export const ZEntryNew = implement<EntryNew>().with({
|
|||
});
|
||||
|
||||
export const ZEntryExecutionNew = implement<EntryExecutionNew>().with({
|
||||
text: fields.TextString(),
|
||||
text: z.string(),
|
||||
done: z.boolean(),
|
||||
});
|
||||
|
||||
export const ZPagination = implement<PaginationRequest>().with({
|
||||
|
@ -122,11 +123,6 @@ export const ZPagination = implement<PaginationRequest>().with({
|
|||
offset: coercedUint.optional(),
|
||||
});
|
||||
|
||||
export const ZSort = z.object({
|
||||
field: z.string().optional(),
|
||||
asc: coercedBool.optional(),
|
||||
});
|
||||
|
||||
const ZFilterListEntry = z.object({
|
||||
id: coercedUint,
|
||||
name: fields.NameString().optional(),
|
||||
|
@ -136,7 +132,7 @@ const paginatedQuery = <T extends z.ZodTypeAny>(f: T) => z
|
|||
.object({
|
||||
filter: f,
|
||||
pagination: ZPagination,
|
||||
sort: ZSort,
|
||||
sort: z.array(z.string()),
|
||||
})
|
||||
.partial();
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ export type EntryExecutionChange = {
|
|||
created_at: Date;
|
||||
|
||||
text: Diff.Change[];
|
||||
done?: boolean;
|
||||
};
|
||||
|
||||
function newOrUndef<T>(o: T, n: T): T | undefined {
|
||||
|
@ -83,6 +84,7 @@ export function executionsDiff(executions: EntryExecution[]): EntryExecutionChan
|
|||
author: v.author,
|
||||
created_at: v.created_at,
|
||||
text,
|
||||
done: v.done,
|
||||
});
|
||||
|
||||
prev = v;
|
||||
|
|
|
@ -16,7 +16,7 @@ it("getQueryUrl", () => {
|
|||
search: "Hello World",
|
||||
},
|
||||
pagination: { limit: 10, offset: 20 },
|
||||
sort: { field: "room", asc: true },
|
||||
sort: ["room"],
|
||||
};
|
||||
|
||||
const queryUrl = getQueryUrl(query, "");
|
||||
|
|
|
@ -5,13 +5,14 @@ import { TRPCClientError } from "@trpc/client";
|
|||
import DOMPurify from "isomorphic-dompurify";
|
||||
import qs from "qs";
|
||||
import type { FormOptions } from "sveltekit-superforms";
|
||||
import type { TRPCClientInit } from "trpc-sveltekit";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { DEFAULT_FILTER_NAME, URL_VISIT } from "$lib/shared/constants";
|
||||
import type { EntityQuery, SavedFilter } from "$lib/shared/model";
|
||||
import { type RouterOutput } from "$lib/shared/trpc";
|
||||
import { trpc, type RouterOutput } from "$lib/shared/trpc";
|
||||
|
||||
import { DateRange } from "./date";
|
||||
import { DateRange, utcDateToYMD } from "./date";
|
||||
import { toastError, toastInfo } from "./toast";
|
||||
|
||||
export function formatBool(val: boolean): string {
|
||||
|
@ -156,3 +157,19 @@ export function defaultVisitUrl(): string {
|
|||
},
|
||||
}, 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),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,23 +5,35 @@ import { superValidate, message } from "sveltekit-superforms";
|
|||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util";
|
||||
|
||||
import { SchemaEntryExecution } from "./schema";
|
||||
import { SchemaNewExecution } from "./editExecution/schema";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => loadWrap(async () => {
|
||||
const form = await superValidate(event.request, SchemaEntryExecution);
|
||||
const formData = await event.request.formData();
|
||||
const form = await superValidate(formData, SchemaNewExecution);
|
||||
if (!form.valid) {
|
||||
return fail(400, { form });
|
||||
}
|
||||
|
||||
const id = ZUrlEntityId.parse(event.params.id);
|
||||
await trpc(event).entry.newExecution.mutate({
|
||||
id,
|
||||
old_execution_id: null,
|
||||
execution: { text: form.data.text },
|
||||
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({
|
||||
id,
|
||||
old_execution_id: form.data.old_execution_id,
|
||||
execution: { text: form.data.text, done: todoDays === null },
|
||||
});
|
||||
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");
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -2,29 +2,29 @@
|
|||
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
|
||||
import { superformConfig } from "$lib/shared/util";
|
||||
|
||||
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 { SchemaEntryExecution } from "./schema";
|
||||
import { SchemaNewExecution } from "./editExecution/schema";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const formData = defaults(SchemaEntryExecution);
|
||||
const {
|
||||
form, errors, enhance,
|
||||
} = superForm(formData, {
|
||||
validators: SchemaEntryExecution,
|
||||
} = superForm(data.form, {
|
||||
validators: SchemaNewExecution,
|
||||
...superformConfig("Eintrag"),
|
||||
});
|
||||
</script>
|
||||
|
||||
<EntryBody entry={data.entry} withExecution />
|
||||
|
||||
{#if !data.entry.execution}
|
||||
{#if !data.entry.execution?.done}
|
||||
<form method="POST" use:enhance>
|
||||
<MarkdownInput
|
||||
name="text"
|
||||
|
@ -33,9 +33,11 @@
|
|||
label="Eintrag erledigen"
|
||||
bind:value={$form.text}
|
||||
>
|
||||
<div class="row c-vlight">
|
||||
<div class="row c-vlight gap-2">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Erledigt</button>
|
||||
<EntryTodoButton />
|
||||
</div>
|
||||
</MarkdownInput>
|
||||
<input name="old_execution_id" type="hidden" bind:value={$form.old_execution_id} />
|
||||
</form>
|
||||
{/if}
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
import type { PageLoad } from "./$types";
|
||||
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
import { SchemaNewExecution } from "./editExecution/schema";
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
const entry = await loadWrap(async () => {
|
||||
const id = ZUrlEntityId.parse(event.params.id);
|
||||
return trpc(event).entry.get.query(id);
|
||||
});
|
||||
|
||||
return { entry };
|
||||
const form = await superValidate(
|
||||
{
|
||||
old_execution_id: entry.execution?.id,
|
||||
...entry.execution,
|
||||
},
|
||||
SchemaNewExecution,
|
||||
);
|
||||
|
||||
return { entry, form };
|
||||
};
|
||||
|
|
|
@ -5,24 +5,33 @@ import { superValidate } from "sveltekit-superforms";
|
|||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util";
|
||||
|
||||
import { SchemaNewExecution } from "./schema";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => loadWrap(async () => {
|
||||
const form = await superValidate(event.request, SchemaNewExecution);
|
||||
const formData = await event.request.formData();
|
||||
const form = await superValidate(formData, SchemaNewExecution);
|
||||
if (!form.valid) {
|
||||
return fail(400, { form });
|
||||
}
|
||||
|
||||
const id = ZUrlEntityId.parse(event.params.id);
|
||||
const todoDays = formData.get("todo");
|
||||
|
||||
await trpc(event).entry.newExecution.mutate({
|
||||
id,
|
||||
execution: form.data,
|
||||
execution: {
|
||||
text: form.data.text,
|
||||
done: todoDays === null,
|
||||
},
|
||||
old_execution_id: form.data.old_execution_id,
|
||||
});
|
||||
|
||||
const nTodoDays = todoDays ? parseInt(todoDays.toString()) : 0;
|
||||
await moveEntryTodoDate(id, nTodoDays, event);
|
||||
|
||||
redirect(302, `/entry/${id}`);
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
|
||||
import { superformConfig } from "$lib/shared/util";
|
||||
|
||||
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 { SchemaNewExecution } from "./schema";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const formData = defaults(SchemaNewExecution);
|
||||
const { form, errors, enhance } = superForm(formData, {
|
||||
const { form, errors, enhance } = superForm(data.form, {
|
||||
validators: SchemaNewExecution,
|
||||
...superformConfig("Eintrag"),
|
||||
});
|
||||
|
@ -30,8 +30,10 @@
|
|||
label="Ausführungstext bearbeiten"
|
||||
bind:value={$form.text}
|
||||
>
|
||||
<div class="row c-vlight">
|
||||
<div class="row c-vlight gap-2">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Speichern</button>
|
||||
<EntryTodoButton />
|
||||
</div>
|
||||
</MarkdownInput>
|
||||
<input name="old_execution_id" type="hidden" bind:value={$form.old_execution_id} />
|
||||
</form>
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
<div class="row c-light text-sm">
|
||||
#{i + 1}
|
||||
<UserField user={version.author} />, {formatDate(version.created_at, true)}
|
||||
{#if !version.done}
|
||||
<div class="badge ellipsis badge-warning ml-2">Notiz</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if version.text.length > 0}
|
||||
<div class="rowb whitespace-pre-wrap">
|
||||
|
|
|
@ -22,7 +22,7 @@ export const load: PageLoad = async (event) => {
|
|||
|
||||
// Sort entries by date
|
||||
if (!query.sort) {
|
||||
query.sort = { field: "date" };
|
||||
query.sort = ["priority:dsc", "date"];
|
||||
}
|
||||
|
||||
const entries = await trpc(event).entry.list.query(query);
|
||||
|
|
|
@ -57,8 +57,8 @@ async function insertTestEntries() {
|
|||
});
|
||||
|
||||
// Execute entries
|
||||
await newEntryExecution(1, eId2, { text: "Some execution txt" });
|
||||
await newEntryExecution(2, eId3, { text: "More execution txt" });
|
||||
await newEntryExecution(1, eId2, { text: "Some execution txt", done: true });
|
||||
await newEntryExecution(2, eId3, { text: "More execution txt", done: true });
|
||||
|
||||
return { eId1, eId2, eId3 };
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ test("create entry execution", async () => {
|
|||
});
|
||||
const text = "Blutabnahme erledigt.";
|
||||
|
||||
const xId = await newEntryExecution(1, eId, { text }, null);
|
||||
const xId = await newEntryExecution(1, eId, { text, done: true }, null);
|
||||
|
||||
const entry = await getEntry(eId);
|
||||
expect(entry.execution?.id).toBe(xId);
|
||||
|
@ -183,8 +183,8 @@ test("create entry execution (update)", async () => {
|
|||
version: TEST_VERSION,
|
||||
});
|
||||
|
||||
const x1 = await newEntryExecution(1, eId, { text: "x1" }, null);
|
||||
const x2 = await newEntryExecution(2, eId, { text: "x2" }, x1);
|
||||
const x1 = await newEntryExecution(1, eId, { text: "x1", done: true }, null);
|
||||
const x2 = await newEntryExecution(2, eId, { text: "x2", done: true }, x1);
|
||||
|
||||
const entry = await getEntry(eId);
|
||||
expect(entry.execution?.id).toBe(x2);
|
||||
|
@ -200,9 +200,9 @@ test("create entry execution (wrong old xid)", async () => {
|
|||
patient_id: 1,
|
||||
version: TEST_VERSION,
|
||||
});
|
||||
const x1 = await newEntryExecution(1, eId, { text: "x1" }, null);
|
||||
const x1 = await newEntryExecution(1, eId, { text: "x1", done: true }, null);
|
||||
|
||||
expect(async () => newEntryExecution(1, eId, { text: "x2" }, x1 + 1)).rejects.toThrowError(new ErrorConflict("old execution id does not match"));
|
||||
expect(async () => newEntryExecution(1, eId, { text: "x2", done: true }, x1 + 1)).rejects.toThrowError(new ErrorConflict("old execution id does not match"));
|
||||
});
|
||||
|
||||
test("get entries", async () => {
|
||||
|
@ -246,8 +246,8 @@ test("get entries", async () => {
|
|||
const entriesPatient = await getEntries({ patient: 1 }, {});
|
||||
expect(entriesPatient.items).length(2);
|
||||
expect(entriesPatient.total).toBe(2);
|
||||
expect(entriesPatient.items[0].id).toBe(eId3);
|
||||
expect(entriesPatient.items[1].id).toBe(eId1);
|
||||
expect(entriesPatient.items[0].id).toBe(eId1);
|
||||
expect(entriesPatient.items[1].id).toBe(eId3);
|
||||
|
||||
// Filter by room
|
||||
const entriesRoom = await getEntries({ room: 1 }, {});
|
||||
|
@ -257,8 +257,8 @@ test("get entries", async () => {
|
|||
const entriesDone = await getEntries({ done: true }, {});
|
||||
expect(entriesDone.items).length(2);
|
||||
expect(entriesDone.total).toBe(2);
|
||||
expect(entriesDone.items[0].id).toBe(eId3);
|
||||
expect(entriesDone.items[1].id).toBe(eId2);
|
||||
expect(entriesDone.items[0].id).toBe(eId2);
|
||||
expect(entriesDone.items[1].id).toBe(eId3);
|
||||
|
||||
// Filter not done
|
||||
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" });
|
||||
expect(entriesDateRange.items).length(2);
|
||||
expect(entriesDateRange.total).toBe(2);
|
||||
expect(entriesDateRange.items[0].id).toBe(eId3);
|
||||
expect(entriesDateRange.items[1].id).toBe(eId2);
|
||||
expect(entriesDateRange.items[0].id).toBe(eId2);
|
||||
expect(entriesDateRange.items[1].id).toBe(eId3);
|
||||
|
||||
// Search
|
||||
const entriesSearch = await getEntries({ search: "Blu" }, {});
|
||||
|
@ -293,6 +293,30 @@ test("get entries", async () => {
|
|||
// NTodo
|
||||
const n = await getNTodo(new Date("2024-01-05"));
|
||||
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 () => {
|
||||
|
@ -304,3 +328,18 @@ test("get category n entries", async () => {
|
|||
await insertTestEntries();
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "$lib/server/query";
|
||||
import { S1, S2 } from "$tests/helpers/testdata";
|
||||
|
||||
const SORT_ID = { field: "id" };
|
||||
const SORT_ID = ["id"];
|
||||
|
||||
test("create patient", async () => {
|
||||
const pId = await newPatient({
|
||||
|
|
Loading…
Reference in a new issue