Compare commits
2 commits
9e7aa15ced
...
16cd49456c
Author | SHA1 | Date | |
---|---|---|---|
16cd49456c | |||
80c8371253 |
19 changed files with 348 additions and 72 deletions
20
src/lib/components/entry/PatientCard.svelte
Normal file
20
src/lib/components/entry/PatientCard.svelte
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { RouterOutput } from "$lib/shared/trpc";
|
||||||
|
import RoomField from "$lib/components/table/RoomField.svelte";
|
||||||
|
|
||||||
|
export let patient: RouterOutput["patient"]["list"]["items"][0];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card2">
|
||||||
|
<div class="row c-light text-sm">Patient</div>
|
||||||
|
<div class="row items-center gap-2">
|
||||||
|
{#if patient.room}
|
||||||
|
<RoomField room={patient.room} />
|
||||||
|
{/if}
|
||||||
|
<a href="/patient/{patient.id}">
|
||||||
|
{patient.first_name}
|
||||||
|
{patient.last_name}
|
||||||
|
({patient.age})
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -92,10 +92,10 @@
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
highlightIndex = i;
|
highlightIndex = i;
|
||||||
}
|
}
|
||||||
if (asTextInput) setInputValue(selection.name || "");
|
if (asTextInput) setInputValue(selection.name ?? "");
|
||||||
} else {
|
} else {
|
||||||
highlightIndex = 0;
|
highlightIndex = 0;
|
||||||
setInputValue(selection.name || "");
|
setInputValue(selection.name ?? "");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
highlightIndex = 0;
|
highlightIndex = 0;
|
||||||
|
|
|
@ -72,7 +72,7 @@ gap-1 pl-1"
|
||||||
{@const hids = hiddenIds()}
|
{@const hids = hiddenIds()}
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
bind:this={autocomplete}
|
bind:this={autocomplete}
|
||||||
items={filter.options || []}
|
items={filter.options ?? []}
|
||||||
hiddenIds={hids}
|
hiddenIds={hids}
|
||||||
{cache}
|
{cache}
|
||||||
cacheKey={filter.id}
|
cacheKey={filter.id}
|
||||||
|
@ -84,7 +84,7 @@ gap-1 pl-1"
|
||||||
fdata.selection = item;
|
fdata.selection = item;
|
||||||
return { close: true, newValue: "" };
|
return { close: true, newValue: "" };
|
||||||
} else {
|
} else {
|
||||||
return { close: false, newValue: item.name || "" };
|
return { close: false, newValue: item.name ?? "" };
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
{onClose}
|
{onClose}
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h1 class="heading">{title}</h1>
|
<h1 class="heading">{title}</h1>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Button on the right side (apply ml-auto) -->
|
||||||
<slot name="rightBtn" />
|
<slot name="rightBtn" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let errors: string[] | undefined = undefined;
|
export let errors: string[] | undefined = undefined;
|
||||||
export let ariaInvalid: boolean | undefined = undefined;
|
export let ariaInvalid: boolean | undefined = undefined;
|
||||||
export let constraints: InputConstraint | undefined = undefined;
|
export let constraints: InputConstraint | undefined = undefined;
|
||||||
|
export let marginTop = false;
|
||||||
|
|
||||||
let editMode = true;
|
let editMode = true;
|
||||||
|
|
||||||
|
@ -17,20 +18,17 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FormField {label} {errors} fullWidth>
|
<div class="card2" class:mt-4={marginTop}>
|
||||||
<button
|
<div class="row c-light text-sm items-center justify-between">
|
||||||
type="button"
|
<span>{label}</span>
|
||||||
slot="topLabel"
|
<button type="button" class="label-text" on:click={toggle} tabindex="-1">
|
||||||
class="label-text"
|
|
||||||
on:click={toggle}
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
|
||||||
{editMode ? "Vorschau" : "Bearbeiten"}
|
{editMode ? "Vorschau" : "Bearbeiten"}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
{#if editMode}
|
{#if editMode}
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea textarea-bordered w-full"
|
class="textarea w-full h-48"
|
||||||
{name}
|
{name}
|
||||||
aria-invalid={ariaInvalid}
|
aria-invalid={ariaInvalid}
|
||||||
bind:value
|
bind:value
|
||||||
|
@ -39,4 +37,14 @@
|
||||||
{:else}
|
{:else}
|
||||||
<Markdown src={value} />
|
<Markdown src={value} />
|
||||||
{/if}
|
{/if}
|
||||||
</FormField>
|
|
||||||
|
{#if errors}
|
||||||
|
<div class="label flex-col items-start">
|
||||||
|
{#each errors as error}
|
||||||
|
<span class="label-text-alt text-error">{error}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
export let data: Pagination<unknown>;
|
export let data: Pagination<unknown>;
|
||||||
export let onUpdate: (pagination: PaginationRequest) => void = () => {};
|
export let onUpdate: (pagination: PaginationRequest) => void = () => {};
|
||||||
|
|
||||||
$: limit = paginationData?.limit || PAGINATION_LIMIT;
|
$: limit = paginationData?.limit ?? PAGINATION_LIMIT;
|
||||||
$: thisPage = Math.floor(data.offset / limit) + 1; // current page number (starting from 1)
|
$: thisPage = Math.floor(data.offset / limit) + 1; // current page number (starting from 1)
|
||||||
$: nPages = Math.ceil(data.total / limit);
|
$: nPages = Math.ceil(data.total / limit);
|
||||||
let windowBottom: number;
|
let windowBottom: number;
|
||||||
|
|
|
@ -105,16 +105,28 @@ export async function newEntryVersion(
|
||||||
throw new ErrorConflict("old version id does not match");
|
throw new ErrorConflict("old version id does not match");
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await tx.entryVersion.create({
|
const updatedVersion = {
|
||||||
data: {
|
|
||||||
// Old version
|
// Old version
|
||||||
entry_id,
|
entry_id,
|
||||||
author_id,
|
author_id,
|
||||||
text: version.text || cver.text,
|
text: version.text ?? cver.text,
|
||||||
date: new Date(version.date || cver.date),
|
date: new Date(version.date ?? cver.date),
|
||||||
category_id: version.category_id || cver.category_id,
|
category_id: version.category_id ?? cver.category_id,
|
||||||
priority: version.priority || cver.priority,
|
priority: version.priority ?? cver.priority,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// Check if there are any updates
|
||||||
|
if (
|
||||||
|
cver.text === updatedVersion.text &&
|
||||||
|
cver.date.getTime() === updatedVersion.date.getTime() &&
|
||||||
|
cver.category_id === updatedVersion.category_id &&
|
||||||
|
cver.priority === updatedVersion.priority
|
||||||
|
) {
|
||||||
|
return cver.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await tx.entryVersion.create({
|
||||||
|
data: updatedVersion,
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
return created.id;
|
return created.id;
|
||||||
|
@ -147,6 +159,11 @@ export async function newEntryExecution(
|
||||||
throw new ErrorConflict("old execution id does not match");
|
throw new ErrorConflict("old execution id does not match");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if there are any updates
|
||||||
|
if (execution.text === cex.text) {
|
||||||
|
return cex.id;
|
||||||
|
}
|
||||||
|
|
||||||
const created = await prisma.entryExecution.create({
|
const created = await prisma.entryExecution.create({
|
||||||
data: {
|
data: {
|
||||||
entry_id,
|
entry_id,
|
||||||
|
|
|
@ -16,12 +16,12 @@ export async function getUser(id: number): Promise<User> {
|
||||||
export async function getUsers(
|
export async function getUsers(
|
||||||
pagination: PaginationRequest
|
pagination: PaginationRequest
|
||||||
): Promise<Pagination<User>> {
|
): Promise<Pagination<User>> {
|
||||||
const offset = pagination.offset || 0;
|
const offset = pagination.offset ?? 0;
|
||||||
const [users, total] = await Promise.all([
|
const [users, total] = await Promise.all([
|
||||||
prisma.user.findMany({
|
prisma.user.findMany({
|
||||||
orderBy: { id: "asc" },
|
orderBy: { id: "asc" },
|
||||||
skip: offset,
|
skip: offset,
|
||||||
take: pagination.limit || PAGINATION_LIMIT,
|
take: pagination.limit ?? PAGINATION_LIMIT,
|
||||||
}),
|
}),
|
||||||
prisma.user.count(),
|
prisma.user.count(),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -26,6 +26,16 @@ export const fields = {
|
||||||
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||||
// @ts-expect-error check date for NaN is valid
|
// @ts-expect-error check date for NaN is valid
|
||||||
.refine((val) => !isNaN(new Date(val))),
|
.refine((val) => !isNaN(new Date(val))),
|
||||||
|
DateStringFuture: () =>
|
||||||
|
fields.DateString().refine(
|
||||||
|
(d) => {
|
||||||
|
const inp = new Date(d);
|
||||||
|
const now = new Date();
|
||||||
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
return inp >= today;
|
||||||
|
},
|
||||||
|
{ message: "Datum muss in der Zukunft liegen" }
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZUrlEntityId = z.coerce.number().int().nonnegative();
|
export const ZUrlEntityId = z.coerce.number().int().nonnegative();
|
||||||
|
|
|
@ -30,18 +30,23 @@ function newOrUndef<T>(o: T, n: T): T | undefined {
|
||||||
return o === n ? undefined : n;
|
return o === n ? undefined : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function doDiffWords(s1: string, s2: string): Diff.Change[] {
|
||||||
|
if (s1 === s2) return [];
|
||||||
|
return diffWords(s1, s2);
|
||||||
|
}
|
||||||
|
|
||||||
export function versionsDiff(versions: EntryVersion[]): EntryVersionChange[] {
|
export function versionsDiff(versions: EntryVersion[]): EntryVersionChange[] {
|
||||||
let prev = versions[versions.length - 1];
|
let prev = versions[versions.length - 1];
|
||||||
const changes: EntryVersionChange[] = [
|
const changes: EntryVersionChange[] = [
|
||||||
{
|
{
|
||||||
...prev,
|
...prev,
|
||||||
text: diffWords("", prev.text),
|
text: [{ value: prev.text }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = versions.length - 2; i >= 0; i--) {
|
for (let i = versions.length - 2; i >= 0; i--) {
|
||||||
const v = versions[i];
|
const v = versions[i];
|
||||||
const text = diffWords(prev.text, v.text);
|
const text = doDiffWords(prev.text, v.text);
|
||||||
|
|
||||||
changes.push({
|
changes.push({
|
||||||
id: v.id,
|
id: v.id,
|
||||||
|
@ -64,13 +69,13 @@ export function executionsDiff(executions: EntryExecution[]): EntryExecutionChan
|
||||||
const changes: EntryExecutionChange[] = [
|
const changes: EntryExecutionChange[] = [
|
||||||
{
|
{
|
||||||
...prev,
|
...prev,
|
||||||
text: diffWords("", prev.text),
|
text: [{ value: prev.text }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = executions.length - 2; i >= 0; i--) {
|
for (let i = executions.length - 2; i >= 0; i--) {
|
||||||
const v = executions[i];
|
const v = executions[i];
|
||||||
const text = diffWords(prev.text, v.text);
|
const text = doDiffWords(prev.text, v.text);
|
||||||
|
|
||||||
changes.push({
|
changes.push({
|
||||||
id: v.id,
|
id: v.id,
|
||||||
|
|
25
src/routes/(app)/entry/[id]/+page.server.ts
Normal file
25
src/routes/(app)/entry/[id]/+page.server.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { superValidate } from "sveltekit-superforms";
|
||||||
|
import type { Actions } from "./$types";
|
||||||
|
import { SchemaEntryExecution } from "./schema";
|
||||||
|
import { fail } from "@sveltejs/kit";
|
||||||
|
import { loadWrap } from "$lib/shared/util";
|
||||||
|
import { trpc } from "$lib/shared/trpc";
|
||||||
|
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async (event) => {
|
||||||
|
const form = await superValidate(event.request, SchemaEntryExecution);
|
||||||
|
if (!form.valid) {
|
||||||
|
return fail(400, { form });
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadWrap(async () => {
|
||||||
|
const id = ZUrlEntityId.parse(event.params.id);
|
||||||
|
await trpc(event).entry.newExecution.mutate({
|
||||||
|
id,
|
||||||
|
old_execution_id: null,
|
||||||
|
execution: { text: form.data.text },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} satisfies Actions;
|
|
@ -2,17 +2,25 @@
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
import { formatDate, humanDate } from "$lib/shared/util";
|
import { formatDate, humanDate } from "$lib/shared/util";
|
||||||
import UserField from "$lib/components/table/UserField.svelte";
|
import UserField from "$lib/components/table/UserField.svelte";
|
||||||
import RoomField from "$lib/components/table/RoomField.svelte";
|
|
||||||
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
||||||
import Markdown from "$lib/components/ui/Markdown.svelte";
|
import Markdown from "$lib/components/ui/Markdown.svelte";
|
||||||
import Header from "$lib/components/ui/Header.svelte";
|
import Header from "$lib/components/ui/Header.svelte";
|
||||||
import Icon from "$lib/components/ui/Icon.svelte";
|
import Icon from "$lib/components/ui/Icon.svelte";
|
||||||
import { mdiPencil } from "@mdi/js";
|
import { mdiPencil } from "@mdi/js";
|
||||||
import VersionsButton from "$lib/components/ui/VersionsButton.svelte";
|
import VersionsButton from "$lib/components/ui/VersionsButton.svelte";
|
||||||
|
import MarkdownInput from "$lib/components/ui/MarkdownInput.svelte";
|
||||||
|
import { SchemaEntryExecution } from "./schema";
|
||||||
|
import { defaults, superForm } from "sveltekit-superforms";
|
||||||
|
import PatientCard from "$lib/components/entry/PatientCard.svelte";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
$: basePath = `/entry/${data.entry.id}`;
|
$: basePath = `/entry/${data.entry.id}`;
|
||||||
|
|
||||||
|
let formData = defaults(SchemaEntryExecution);
|
||||||
|
const { form, errors, enhance } = superForm(formData, {
|
||||||
|
validators: SchemaEntryExecution,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
@ -26,6 +34,9 @@
|
||||||
{#if data.entry.current_version.priority}
|
{#if data.entry.current_version.priority}
|
||||||
<div class="badge ellipsis badge-warning">Priorität</div>
|
<div class="badge ellipsis badge-warning">Priorität</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<a slot="rightBtn" href="{basePath}/edit" class="btn btn-sm btn-primary ml-auto">
|
||||||
|
Bearbeiten
|
||||||
|
</a>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<p class="text-sm flex flex-row gap-2">
|
<p class="text-sm flex flex-row gap-2">
|
||||||
|
@ -34,28 +45,16 @@
|
||||||
<span>Zu erledigen {humanDate(data.entry.current_version.date)}</span>
|
<span>Zu erledigen {humanDate(data.entry.current_version.date)}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="card2">
|
<PatientCard patient={data.entry.patient} />
|
||||||
<div class="row c-light text-sm">Patient</div>
|
|
||||||
<div class="row items-center gap-2">
|
|
||||||
{#if data.entry.patient.room}
|
|
||||||
<RoomField room={data.entry.patient.room} />
|
|
||||||
{/if}
|
|
||||||
<a href="/patient/{data.entry.patient.id}">
|
|
||||||
{data.entry.patient.first_name}
|
|
||||||
{data.entry.patient.last_name}
|
|
||||||
({data.entry.patient.age})
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card2">
|
<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">
|
||||||
Beschreibung
|
Beschreibung
|
||||||
<div>
|
<div>
|
||||||
<VersionsButton href="{basePath}/versions" n={data.entry.n_versions} />
|
<VersionsButton href="{basePath}/versions" n={data.entry.n_versions} />
|
||||||
<button class="btn btn-circle btn-sm btn-ghost">
|
<a href="{basePath}/edit" class="btn btn-circle btn-sm btn-ghost">
|
||||||
<Icon path={mdiPencil} size={1.2} />
|
<Icon path={mdiPencil} size={1.2} />
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -64,8 +63,10 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row c-vlight text-sm">
|
<div class="row c-vlight text-sm">
|
||||||
Zuletzt bearbeitet am {formatDate(data.entry.current_version.created_at, true)} von
|
<p>
|
||||||
|
Zuletzt bearbeitet am {formatDate(data.entry.current_version.created_at, true)} von
|
||||||
<UserField user={data.entry.current_version.author} filterName="author" />
|
<UserField user={data.entry.current_version.author} filterName="author" />
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -74,19 +75,35 @@
|
||||||
<div class="row c-light text-sm items-center justify-between">
|
<div class="row c-light text-sm items-center justify-between">
|
||||||
<p>
|
<p>
|
||||||
Erledigt am {formatDate(data.entry.execution.created_at, true)} von
|
Erledigt am {formatDate(data.entry.execution.created_at, true)} von
|
||||||
<UserField user={data.entry.execution.author} filterName="executor" />:
|
<UserField user={data.entry.execution.author} filterName="executor" />
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<VersionsButton href="{basePath}/executions" n={data.entry.n_executions} />
|
<VersionsButton href="{basePath}/executions" n={data.entry.n_executions} />
|
||||||
<button class="btn btn-circle btn-xs btn-ghost">
|
<a href="{basePath}/editExecution" class="btn btn-circle btn-xs btn-ghost">
|
||||||
<Icon path={mdiPencil} size={1.2} />
|
<Icon path={mdiPencil} size={1.2} />
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#if data.entry.execution?.text}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p class="prose">
|
<p class="prose">
|
||||||
<Markdown src={data.entry.execution?.text} />
|
<Markdown src={data.entry.execution?.text} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<form method="POST" use:enhance>
|
||||||
|
<MarkdownInput
|
||||||
|
label="Eintrag erledigen"
|
||||||
|
name="text"
|
||||||
|
ariaInvalid={Boolean($errors.text)}
|
||||||
|
bind:value={$form.text}
|
||||||
|
errors={$errors.text}
|
||||||
|
>
|
||||||
|
<div class="row c-vlight">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">Erledigt</button>
|
||||||
|
</div>
|
||||||
|
</MarkdownInput>
|
||||||
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
28
src/routes/(app)/entry/[id]/edit/+page.server.ts
Normal file
28
src/routes/(app)/entry/[id]/edit/+page.server.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { superValidate } from "sveltekit-superforms";
|
||||||
|
import type { Actions } from "./$types";
|
||||||
|
import { SchemaNewEntryVersion } from "./schema";
|
||||||
|
import { fail, redirect } from "@sveltejs/kit";
|
||||||
|
import { loadWrap } from "$lib/shared/util";
|
||||||
|
import { trpc } from "$lib/shared/trpc";
|
||||||
|
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async (event) => {
|
||||||
|
const form = await superValidate(event.request, SchemaNewEntryVersion);
|
||||||
|
if (!form.valid) {
|
||||||
|
return fail(400, { form });
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryId = await loadWrap(async () => {
|
||||||
|
const id = ZUrlEntityId.parse(event.params.id);
|
||||||
|
await trpc(event).entry.newVersion.mutate({
|
||||||
|
id,
|
||||||
|
version: form.data,
|
||||||
|
old_version_id: form.data.old_version_id,
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
|
||||||
|
throw redirect(302, `/entry/${entryId}`);
|
||||||
|
},
|
||||||
|
} satisfies Actions;
|
103
src/routes/(app)/entry/[id]/edit/+page.svelte
Normal file
103
src/routes/(app)/entry/[id]/edit/+page.svelte
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
|
import Header from "$lib/components/ui/Header.svelte";
|
||||||
|
import PatientCard from "$lib/components/entry/PatientCard.svelte";
|
||||||
|
import { formatDate, humanDate } from "$lib/shared/util";
|
||||||
|
import FormField from "$lib/components/ui/FormField.svelte";
|
||||||
|
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
||||||
|
import { trpc } from "$lib/shared/trpc";
|
||||||
|
import { superForm } from "sveltekit-superforms";
|
||||||
|
import { SchemaNewEntryVersion } from "./schema";
|
||||||
|
import MarkdownInput from "$lib/components/ui/MarkdownInput.svelte";
|
||||||
|
import UserField from "$lib/components/table/UserField.svelte";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
$: basePath = `/entry/${data.entry.id}`;
|
||||||
|
|
||||||
|
const { form, errors, constraints, enhance, tainted } = superForm(data.form, {
|
||||||
|
validators: SchemaNewEntryVersion,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Eintrag #{data.entry.id}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Header title="Eintrag #{data.entry.id} bearbeiten" backHref={basePath}></Header>
|
||||||
|
|
||||||
|
<p class="text-sm flex flex-row gap-2">
|
||||||
|
<span>Erstellt {humanDate(data.entry.created_at, true)}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>
|
||||||
|
Zuletzt bearbeitet am {formatDate(data.entry.current_version.created_at, true)} von
|
||||||
|
<UserField user={data.entry.current_version.author} filterName="author" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<PatientCard patient={data.entry.patient} />
|
||||||
|
|
||||||
|
<form method="POST" use:enhance>
|
||||||
|
<input type="hidden" name="old_version_id" value={$form.old_version_id} />
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<FormField label="Kategorie" errors={$errors.category_id}>
|
||||||
|
<Autocomplete
|
||||||
|
inputCls="input input-bordered w-full max-w-xs"
|
||||||
|
items={async () => {
|
||||||
|
return await trpc().category.list.query();
|
||||||
|
}}
|
||||||
|
selection={data.entry.current_version.category}
|
||||||
|
onSelect={(item) => {
|
||||||
|
// @ts-expect-error ids are always numeric
|
||||||
|
$form.category_id = item.id;
|
||||||
|
return { newValue: item.name ?? "", close: true };
|
||||||
|
}}
|
||||||
|
onUnselect={() => {
|
||||||
|
$form.category_id = null;
|
||||||
|
}}
|
||||||
|
asTextInput
|
||||||
|
idInputName="category_id"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Zu erledigen am" errors={$errors.date}>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="date"
|
||||||
|
aria-invalid={Boolean($errors.date)}
|
||||||
|
bind:value={$form.date}
|
||||||
|
{...$constraints.date}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div class="form-control w-full max-w-xs">
|
||||||
|
<label class="label cursor-pointer gap-2 justify-start">
|
||||||
|
<span class="label-text text-right">Priorität</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="priority"
|
||||||
|
class="checkbox checkbox-warning"
|
||||||
|
bind:checked={$form.priority}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MarkdownInput
|
||||||
|
label="Beschreibung"
|
||||||
|
name="text"
|
||||||
|
marginTop
|
||||||
|
ariaInvalid={Boolean($errors.text)}
|
||||||
|
bind:value={$form.text}
|
||||||
|
errors={$errors.text}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-primary max-w-32 mt-4"
|
||||||
|
type="submit"
|
||||||
|
disabled={browser && $tainted === undefined}>Speichern</button
|
||||||
|
>
|
||||||
|
</form>
|
23
src/routes/(app)/entry/[id]/edit/+page.ts
Normal file
23
src/routes/(app)/entry/[id]/edit/+page.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||||
|
import { trpc } from "$lib/shared/trpc";
|
||||||
|
import { loadWrap } from "$lib/shared/util";
|
||||||
|
import { superValidate } from "sveltekit-superforms";
|
||||||
|
import type { PageLoad } from "./$types";
|
||||||
|
import { SchemaNewEntryVersion } from "./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);
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = await superValidate(
|
||||||
|
{
|
||||||
|
old_version_id: entry.current_version.id,
|
||||||
|
...entry.current_version,
|
||||||
|
},
|
||||||
|
SchemaNewEntryVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
return { entry, form };
|
||||||
|
};
|
11
src/routes/(app)/entry/[id]/edit/schema.ts
Normal file
11
src/routes/(app)/entry/[id]/edit/schema.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { ZEntryVersionNew, fields } from "$lib/shared/model/validation";
|
||||||
|
import { zod } from "sveltekit-superforms/adapters";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const SchemaNewEntryVersion = zod(
|
||||||
|
ZEntryVersionNew.extend({
|
||||||
|
old_version_id: fields.EntityId(),
|
||||||
|
// Override priority field so checkbox value is always true/false, not optional
|
||||||
|
priority: z.boolean(),
|
||||||
|
})
|
||||||
|
);
|
9
src/routes/(app)/entry/[id]/schema.ts
Normal file
9
src/routes/(app)/entry/[id]/schema.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { fields } from "$lib/shared/model/validation";
|
||||||
|
import { zod } from "sveltekit-superforms/adapters";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const ZEntryDone = z.object({
|
||||||
|
text: fields.TextString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SchemaEntryExecution = zod(ZEntryDone);
|
|
@ -7,7 +7,6 @@
|
||||||
import { SchemaNewEntryWithPatient } from "./schema";
|
import { SchemaNewEntryWithPatient } from "./schema";
|
||||||
|
|
||||||
let formData = defaults(SchemaNewEntryWithPatient);
|
let formData = defaults(SchemaNewEntryWithPatient);
|
||||||
|
|
||||||
const { form, errors, constraints, enhance } = superForm(formData, {
|
const { form, errors, constraints, enhance } = superForm(formData, {
|
||||||
validators: SchemaNewEntryWithPatient,
|
validators: SchemaNewEntryWithPatient,
|
||||||
});
|
});
|
||||||
|
@ -158,6 +157,7 @@
|
||||||
<MarkdownInput
|
<MarkdownInput
|
||||||
label="Beschreibung"
|
label="Beschreibung"
|
||||||
name="text"
|
name="text"
|
||||||
|
marginTop
|
||||||
ariaInvalid={Boolean($errors.text)}
|
ariaInvalid={Boolean($errors.text)}
|
||||||
bind:value={$form.text}
|
bind:value={$form.text}
|
||||||
errors={$errors.text}
|
errors={$errors.text}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { dateToYMD } from "$lib/shared/util";
|
||||||
|
|
||||||
const emsg = "Erforderlich für einen neuen Patienten";
|
const emsg = "Erforderlich für einen neuen Patienten";
|
||||||
|
|
||||||
export const ZNewEntryWithPatient = z
|
const ZNewEntryWithPatient = z
|
||||||
.object({
|
.object({
|
||||||
room_id: fields.EntityId().nullable(),
|
room_id: fields.EntityId().nullable(),
|
||||||
patient_id: fields.EntityId().nullable(),
|
patient_id: fields.EntityId().nullable(),
|
||||||
|
@ -13,7 +13,7 @@ export const ZNewEntryWithPatient = z
|
||||||
patient_last_name: fields.NameString().nullable(),
|
patient_last_name: fields.NameString().nullable(),
|
||||||
patient_age: fields.Age().nullable(),
|
patient_age: fields.Age().nullable(),
|
||||||
category_id: fields.EntityId().nullable(),
|
category_id: fields.EntityId().nullable(),
|
||||||
date: fields.DateString().default(() => dateToYMD(new Date())),
|
date: fields.DateStringFuture().default(() => dateToYMD(new Date())),
|
||||||
priority: z.boolean(),
|
priority: z.boolean(),
|
||||||
text: fields.TextString(),
|
text: fields.TextString(),
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue