Compare commits
2 commits
22b74987e5
...
9e7aa15ced
Author | SHA1 | Date | |
---|---|---|---|
9e7aa15ced | |||
4501e65837 |
14 changed files with 318 additions and 70 deletions
|
@ -33,7 +33,6 @@ button {
|
||||||
.card2 {
|
.card2 {
|
||||||
@apply bg-base-200;
|
@apply bg-base-200;
|
||||||
@apply rounded-xl;
|
@apply rounded-xl;
|
||||||
@apply mb-8;
|
|
||||||
@apply flex flex-col;
|
@apply flex flex-col;
|
||||||
@apply border-solid border-base-content/30 border-[1px];
|
@apply border-solid border-base-content/30 border-[1px];
|
||||||
|
|
||||||
|
@ -51,10 +50,14 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-light {
|
.c-light {
|
||||||
@apply bg-base-content/20;
|
@apply bg-base-content/20 py-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-vlight {
|
||||||
|
@apply bg-base-content/10 py-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-primary {
|
.c-primary {
|
||||||
@apply bg-primary text-primary-content;
|
@apply bg-primary text-primary-content py-1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
export let backHref: string | undefined = undefined;
|
export let backHref: string | undefined = undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-4 flex flex-row">
|
<div class="flex flex-row">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
{#if backHref}
|
{#if backHref}
|
||||||
<a href={backHref} class="btn btn-sm btn-circle btn-ghost">
|
<a href={backHref} class="btn btn-sm btn-circle btn-ghost">
|
||||||
|
|
15
src/lib/components/ui/VersionsButton.svelte
Normal file
15
src/lib/components/ui/VersionsButton.svelte
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- Button showing the amount of entry versions -->
|
||||||
|
<script lang="ts">
|
||||||
|
import { mdiHistory } from "@mdi/js";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
|
||||||
|
export let n: number;
|
||||||
|
export let href: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if n > 1}
|
||||||
|
<a {href} class="btn btn-xs btn-primary rounded-full">
|
||||||
|
<Icon path={mdiHistory} size={1.2} />
|
||||||
|
<span>{n}</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
|
@ -39,6 +39,14 @@ export async function getEntry(id: number): Promise<Entry> {
|
||||||
return mapEntry(entry);
|
return mapEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getEntryNVersions(id: number): Promise<number> {
|
||||||
|
return prisma.entryVersion.count({ where: { entry_id: id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEntryNExecutions(id: number): Promise<number> {
|
||||||
|
return prisma.entryExecution.count({ where: { entry_id: id } });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getEntryVersions(id: number): Promise<EntryVersion[]> {
|
export async function getEntryVersions(id: number): Promise<EntryVersion[]> {
|
||||||
const versions = await prisma.entryVersion.findMany({
|
const versions = await prisma.entryVersion.findMany({
|
||||||
where: { entry_id: id },
|
where: { entry_id: id },
|
||||||
|
|
|
@ -12,19 +12,28 @@ import {
|
||||||
getEntries,
|
getEntries,
|
||||||
getEntry,
|
getEntry,
|
||||||
getEntryExecutions,
|
getEntryExecutions,
|
||||||
|
getEntryNExecutions,
|
||||||
|
getEntryNVersions,
|
||||||
getEntryVersions,
|
getEntryVersions,
|
||||||
newEntry,
|
newEntry,
|
||||||
newEntryExecution,
|
newEntryExecution,
|
||||||
newEntryVersion,
|
newEntryVersion,
|
||||||
} from "$lib/server/query";
|
} from "$lib/server/query";
|
||||||
import { versionsDiff } from "$lib/shared/util/diff";
|
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
|
||||||
|
|
||||||
const ZEntityId = fields.EntityId();
|
const ZEntityId = fields.EntityId();
|
||||||
|
|
||||||
export const entryRouter = t.router({
|
export const entryRouter = t.router({
|
||||||
get: t.procedure
|
get: t.procedure.input(ZEntityId).query(async (opts) =>
|
||||||
.input(ZEntityId)
|
trpcWrap(async () => {
|
||||||
.query(async (opts) => trpcWrap(async () => getEntry(opts.input))),
|
const [entry, n_versions, n_executions] = await Promise.all([
|
||||||
|
getEntry(opts.input),
|
||||||
|
getEntryNVersions(opts.input),
|
||||||
|
getEntryNExecutions(opts.input),
|
||||||
|
]);
|
||||||
|
return { ...entry, n_versions, n_executions };
|
||||||
|
})
|
||||||
|
),
|
||||||
list: t.procedure
|
list: t.procedure
|
||||||
.input(ZEntriesQuery)
|
.input(ZEntriesQuery)
|
||||||
.query(async (opts) =>
|
.query(async (opts) =>
|
||||||
|
@ -48,6 +57,12 @@ export const entryRouter = t.router({
|
||||||
executions: t.procedure
|
executions: t.procedure
|
||||||
.input(ZEntityId)
|
.input(ZEntityId)
|
||||||
.query(async (opts) => trpcWrap(async () => getEntryExecutions(opts.input))),
|
.query(async (opts) => trpcWrap(async () => getEntryExecutions(opts.input))),
|
||||||
|
executionsDiff: t.procedure.input(ZEntityId).query(async (opts) =>
|
||||||
|
trpcWrap(async () => {
|
||||||
|
const executions = await getEntryExecutions(opts.input);
|
||||||
|
return executionsDiff(executions);
|
||||||
|
})
|
||||||
|
),
|
||||||
create: t.procedure
|
create: t.procedure
|
||||||
.input(ZEntryNew)
|
.input(ZEntryNew)
|
||||||
.mutation(async (opts) =>
|
.mutation(async (opts) =>
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { diffWords } from "diff";
|
import { diffWords } from "diff";
|
||||||
import type { EntryVersion, Category, Option, UserTag } from "$lib/shared/model";
|
import type {
|
||||||
|
EntryVersion,
|
||||||
|
EntryExecution,
|
||||||
|
Category,
|
||||||
|
Option,
|
||||||
|
UserTag,
|
||||||
|
} from "$lib/shared/model";
|
||||||
|
|
||||||
export type EntryVersionChange = {
|
export type EntryVersionChange = {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -12,6 +18,14 @@ export type EntryVersionChange = {
|
||||||
priority?: boolean;
|
priority?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EntryExecutionChange = {
|
||||||
|
id: number;
|
||||||
|
author: UserTag;
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
text: Diff.Change[];
|
||||||
|
};
|
||||||
|
|
||||||
function newOrUndef<T>(o: T, n: T): T | undefined {
|
function newOrUndef<T>(o: T, n: T): T | undefined {
|
||||||
return o === n ? undefined : n;
|
return o === n ? undefined : n;
|
||||||
}
|
}
|
||||||
|
@ -44,3 +58,29 @@ export function versionsDiff(versions: EntryVersion[]): EntryVersionChange[] {
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function executionsDiff(executions: EntryExecution[]): EntryExecutionChange[] {
|
||||||
|
let prev = executions[executions.length - 1];
|
||||||
|
const changes: EntryExecutionChange[] = [
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
text: diffWords("", prev.text),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = executions.length - 2; i >= 0; i--) {
|
||||||
|
const v = executions[i];
|
||||||
|
const text = diffWords(prev.text, v.text);
|
||||||
|
|
||||||
|
changes.push({
|
||||||
|
id: v.id,
|
||||||
|
author: v.author,
|
||||||
|
created_at: v.created_at,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
|
||||||
|
prev = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
32
src/lib/shared/util/index.test.ts
Normal file
32
src/lib/shared/util/index.test.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { expect, test, vi } from "vitest";
|
||||||
|
import { humanDate } from ".";
|
||||||
|
|
||||||
|
const MINUTE = 60000;
|
||||||
|
const HOUR = 3_600_000;
|
||||||
|
const DAY = 24 * HOUR;
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ s: 0, txt: "jetzt gerade" },
|
||||||
|
{ s: -30 * MINUTE, txt: "vor 30 Minuten" },
|
||||||
|
{ s: -11 * HOUR, txt: "vor 11 Stunden" },
|
||||||
|
{ s: -DAY, txt: "gestern" },
|
||||||
|
{ s: -2.5 * DAY, txt: "vor 2 Tagen" },
|
||||||
|
{ s: -2.6 * DAY, txt: "vor 3 Tagen" },
|
||||||
|
{ s: -3.5 * DAY, txt: "vor 3 Tagen" },
|
||||||
|
{ s: -4 * DAY, txt: "am 28.12.2023, 12:00" },
|
||||||
|
|
||||||
|
{ s: 28 * MINUTE, txt: "in 28 Minuten" },
|
||||||
|
{ s: 8 * HOUR, txt: "in 8 Stunden" },
|
||||||
|
{ s: DAY, txt: "morgen" },
|
||||||
|
{ s: 2.6 * DAY, txt: "in 3 Tagen" },
|
||||||
|
{ s: 4 * DAY, txt: "am 05.01.2024, 12:00" },
|
||||||
|
])("humanDate", ({ s, txt }) => {
|
||||||
|
const mockDate = new Date(2024, 0, 1, 12, 0);
|
||||||
|
vi.setSystemTime(mockDate);
|
||||||
|
|
||||||
|
const dt = new Date(Number(mockDate) + s);
|
||||||
|
const res = humanDate(dt, true);
|
||||||
|
expect(res).toBe(txt);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
|
@ -4,13 +4,19 @@ import { TRPCClientError } from "@trpc/client";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
export function formatDate(date: Date | string, time = false): string {
|
const LOCALE = "de-DE";
|
||||||
let dt = date;
|
|
||||||
if (!(dt instanceof Date)) {
|
function coerceDate(date: Date | string): Date {
|
||||||
dt = new Date(dt);
|
if (!(date instanceof Date)) {
|
||||||
|
return new Date(date);
|
||||||
}
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(date: Date | string, time = false): string {
|
||||||
|
const dt = coerceDate(date);
|
||||||
if (time) {
|
if (time) {
|
||||||
return dt.toLocaleString("de-DE", {
|
return dt.toLocaleString(LOCALE, {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
@ -18,7 +24,7 @@ export function formatDate(date: Date | string, time = false): string {
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return dt.toLocaleDateString("de-DE", {
|
return dt.toLocaleDateString(LOCALE, {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
@ -26,6 +32,42 @@ export function formatDate(date: Date | string, time = false): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MS_PER_DAY = 86400000;
|
||||||
|
|
||||||
|
function dateDiffInDays(a: Date, b: Date): number {
|
||||||
|
const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
|
||||||
|
const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
|
||||||
|
|
||||||
|
return Math.round((utc2 - utc1) / MS_PER_DAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanDate(date: Date | string, time = false): string {
|
||||||
|
const now = new Date();
|
||||||
|
const dt = coerceDate(date);
|
||||||
|
const threshold = 302400000; // 3.5 * 24 * 3_600_000
|
||||||
|
const diff = Number(dt) - Number(now); // pos: Future, neg: Past
|
||||||
|
if (Math.abs(diff) > threshold) return "am " + formatDate(date, time);
|
||||||
|
|
||||||
|
const intl = new Intl.RelativeTimeFormat(LOCALE);
|
||||||
|
|
||||||
|
const diffDays = dateDiffInDays(now, dt);
|
||||||
|
if (diffDays !== 0) {
|
||||||
|
if (diffDays === 1) return "morgen";
|
||||||
|
if (diffDays === -1) return "gestern";
|
||||||
|
return intl.format(diffDays, "day");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time) {
|
||||||
|
const diffHours = Math.round(diff / 3_600_000);
|
||||||
|
if (diffHours !== 0) return intl.format(diffHours, "hour");
|
||||||
|
|
||||||
|
const diffMinutes = Math.round(diff / 60_000);
|
||||||
|
if (diffMinutes !== 0) return intl.format(diffMinutes, "minute");
|
||||||
|
}
|
||||||
|
|
||||||
|
return time ? "jetzt gerade" : "heute";
|
||||||
|
}
|
||||||
|
|
||||||
export function formatBool(val: boolean): string {
|
export function formatBool(val: boolean): string {
|
||||||
return val ? "Ja" : "Nein";
|
return val ? "Ja" : "Nein";
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,6 @@
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-[100vw] p-4 pb-8">
|
<div class="max-w-[100vw] p-4 pb-8 flex flex-col gap-4">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
import { formatDate } 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 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 { mdiHistory } from "@mdi/js";
|
import { mdiPencil } from "@mdi/js";
|
||||||
|
import VersionsButton from "$lib/components/ui/VersionsButton.svelte";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
$: basePath = `/entry/${data.entry.id}`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
@ -23,45 +26,62 @@
|
||||||
{#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
|
|
||||||
href="/entry/{data.entry.id}/versions"
|
|
||||||
class="btn btn-sm btn-primary ml-auto"
|
|
||||||
slot="rightBtn"
|
|
||||||
>
|
|
||||||
<Icon path={mdiHistory} />
|
|
||||||
<span class="hidden sm:inline">Versionen</span>
|
|
||||||
</a>
|
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
|
<p class="text-sm flex flex-row gap-2">
|
||||||
|
<span>Erstellt {humanDate(data.entry.created_at, true)}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>Zu erledigen {humanDate(data.entry.current_version.date)}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="card2">
|
<div class="card2">
|
||||||
<div class="row c-light text-sm">Patient</div>
|
<div class="row c-light text-sm">Patient</div>
|
||||||
<div class="row items-center gap-2">
|
<div class="row items-center gap-2">
|
||||||
{#if data.entry.patient.room}
|
{#if data.entry.patient.room}
|
||||||
<RoomField room={data.entry.patient.room} />
|
<RoomField room={data.entry.patient.room} />
|
||||||
{/if}
|
{/if}
|
||||||
{data.entry.patient.first_name}
|
<a href="/patient/{data.entry.patient.id}">
|
||||||
{data.entry.patient.last_name}
|
{data.entry.patient.first_name}
|
||||||
({data.entry.patient.age})
|
{data.entry.patient.last_name}
|
||||||
|
({data.entry.patient.age})
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card2">
|
<div class="card2">
|
||||||
<div class="row c-light text-sm">Beschreibung</div>
|
<div class="row c-light text-sm items-center justify-between">
|
||||||
|
Beschreibung
|
||||||
|
<div>
|
||||||
|
<VersionsButton href="{basePath}/versions" n={data.entry.n_versions} />
|
||||||
|
<button class="btn btn-circle btn-sm btn-ghost">
|
||||||
|
<Icon path={mdiPencil} size={1.2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p class="prose">
|
<p class="prose">
|
||||||
<Markdown src={data.entry.current_version.text} />
|
<Markdown src={data.entry.current_version.text} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row c-vlight text-sm">
|
||||||
|
Zuletzt bearbeitet am {formatDate(data.entry.current_version.created_at, true)} von
|
||||||
|
<UserField user={data.entry.current_version.author} filterName="author" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if data.entry.execution}
|
{#if data.entry.execution}
|
||||||
<div class="card2">
|
<div class="card2">
|
||||||
<div class="row c-light text-sm">
|
<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>
|
||||||
|
<VersionsButton href="{basePath}/executions" n={data.entry.n_executions} />
|
||||||
|
<button class="btn btn-circle btn-xs btn-ghost">
|
||||||
|
<Icon path={mdiPencil} size={1.2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p class="prose">
|
<p class="prose">
|
||||||
|
|
45
src/routes/(app)/entry/[id]/executions/+page.svelte
Normal file
45
src/routes/(app)/entry/[id]/executions/+page.svelte
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
||||||
|
import UserField from "$lib/components/table/UserField.svelte";
|
||||||
|
import { formatBool, formatDate } from "$lib/shared/util";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
import Header from "$lib/components/ui/Header.svelte";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
$: entryId = $page.params.id;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Eintrag #{entryId} - Erledigt</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Header title="Eintrag #{entryId} - Erledigt" backHref="/entry/{entryId}" />
|
||||||
|
|
||||||
|
{#each data.versions as version, i}
|
||||||
|
<div class="card2">
|
||||||
|
<div class="row c-light text-sm">
|
||||||
|
#{i + 1}
|
||||||
|
<UserField user={version.author} />, {formatDate(version.created_at, true)}
|
||||||
|
</div>
|
||||||
|
{#if version.text.length > 0}
|
||||||
|
<div class="row whitespace-pre-wrap">
|
||||||
|
{#each version.text as change}
|
||||||
|
<span class:added={change.added} class:removed={change.removed}>
|
||||||
|
{change.value}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.added {
|
||||||
|
@apply bg-success/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removed {
|
||||||
|
@apply bg-error/20;
|
||||||
|
}
|
||||||
|
</style>
|
13
src/routes/(app)/entry/[id]/executions/+page.ts
Normal file
13
src/routes/(app)/entry/[id]/executions/+page.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||||
|
import { trpc } from "$lib/shared/trpc";
|
||||||
|
import { loadWrap } from "$lib/shared/util";
|
||||||
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
|
export const load: PageLoad = async (event) => {
|
||||||
|
const id = ZUrlEntityId.parse(event.params.id);
|
||||||
|
const versions = await loadWrap(async () =>
|
||||||
|
trpc(event).entry.executionsDiff.query(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { versions };
|
||||||
|
};
|
|
@ -16,45 +16,44 @@
|
||||||
|
|
||||||
<Header title="Eintrag #{entryId} - Versionen" backHref="/entry/{entryId}" />
|
<Header title="Eintrag #{entryId} - Versionen" backHref="/entry/{entryId}" />
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
{#each data.versions as version, i}
|
||||||
{#each data.versions as version}
|
<div class="card2">
|
||||||
<div class="card2">
|
<div class="row c-light text-sm">
|
||||||
<div class="row c-light text-sm">
|
#{i + 1}
|
||||||
<UserField user={version.author} />, {formatDate(version.created_at, true)}
|
<UserField user={version.author} />, {formatDate(version.created_at, true)}
|
||||||
</div>
|
|
||||||
{#if version.category}
|
|
||||||
<div class="row">
|
|
||||||
<div>Kategeorie</div>
|
|
||||||
<div><CategoryField category={version.category} /></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if version.text.length > 0}
|
|
||||||
<div class="row">
|
|
||||||
<div>Text</div>
|
|
||||||
<div class="whitespace-pre-wrap">
|
|
||||||
{#each version.text as change}
|
|
||||||
<span class:added={change.added} class:removed={change.removed}>
|
|
||||||
{change.value}
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if version.date !== undefined}
|
|
||||||
<div class="row">
|
|
||||||
<div>Datum</div>
|
|
||||||
<div>{formatDate(version.date)}</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if version.priority !== undefined}
|
|
||||||
<div class="row">
|
|
||||||
<div>Priorität</div>
|
|
||||||
<div>{formatBool(version.priority)}</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{#if version.category}
|
||||||
</div>
|
<div class="row">
|
||||||
|
<div>Kategeorie</div>
|
||||||
|
<div><CategoryField category={version.category} /></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if version.text.length > 0}
|
||||||
|
<div class="row">
|
||||||
|
<div>Text</div>
|
||||||
|
<div class="whitespace-pre-wrap">
|
||||||
|
{#each version.text as change}
|
||||||
|
<span class:added={change.added} class:removed={change.removed}>
|
||||||
|
{change.value}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if version.date !== undefined}
|
||||||
|
<div class="row">
|
||||||
|
<div>Datum</div>
|
||||||
|
<div>{formatDate(version.date)}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if version.priority !== undefined}
|
||||||
|
<div class="row">
|
||||||
|
<div>Priorität</div>
|
||||||
|
<div>{formatBool(version.priority)}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.row > div:first-child {
|
.row > div:first-child {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {
|
import {
|
||||||
getEntries,
|
getEntries,
|
||||||
getEntry,
|
getEntry,
|
||||||
|
getEntryNExecutions,
|
||||||
|
getEntryNVersions,
|
||||||
getEntryVersions,
|
getEntryVersions,
|
||||||
newEntry,
|
newEntry,
|
||||||
newEntryExecution,
|
newEntryExecution,
|
||||||
|
@ -32,6 +34,11 @@ test("create entry", async () => {
|
||||||
expect(entry.current_version.text).toBe(TEST_VERSION.text);
|
expect(entry.current_version.text).toBe(TEST_VERSION.text);
|
||||||
expect(entry.current_version.date).toStrictEqual(TEST_VERSION.date);
|
expect(entry.current_version.date).toStrictEqual(TEST_VERSION.date);
|
||||||
expect(entry.current_version.priority).toBe(TEST_VERSION.priority);
|
expect(entry.current_version.priority).toBe(TEST_VERSION.priority);
|
||||||
|
|
||||||
|
const nVersions = await getEntryNVersions(eId);
|
||||||
|
expect(nVersions).toBe(1);
|
||||||
|
const nExecutions = await getEntryNExecutions(eId);
|
||||||
|
expect(nExecutions).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create entry version", async () => {
|
test("create entry version", async () => {
|
||||||
|
@ -65,6 +72,9 @@ test("create entry version", async () => {
|
||||||
expect(history[0]).toMatchObject(expectedVersion);
|
expect(history[0]).toMatchObject(expectedVersion);
|
||||||
expect(history[1].text).toBe(TEST_VERSION.text);
|
expect(history[1].text).toBe(TEST_VERSION.text);
|
||||||
expect(history[0].created_at).greaterThan(history[1].created_at);
|
expect(history[0].created_at).greaterThan(history[1].created_at);
|
||||||
|
|
||||||
|
const nVersions = await getEntryNVersions(eId);
|
||||||
|
expect(nVersions).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create entry version (partial)", async () => {
|
test("create entry version (partial)", async () => {
|
||||||
|
@ -117,6 +127,9 @@ test("create entry execution", async () => {
|
||||||
expect(entry.execution?.id).toBe(xId);
|
expect(entry.execution?.id).toBe(xId);
|
||||||
expect(entry.execution?.author.id).toBe(1);
|
expect(entry.execution?.author.id).toBe(1);
|
||||||
expect(entry.execution?.text).toBe(text);
|
expect(entry.execution?.text).toBe(text);
|
||||||
|
|
||||||
|
const nExecutions = await getEntryNExecutions(eId);
|
||||||
|
expect(nExecutions).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create entry execution (update)", async () => {
|
test("create entry execution (update)", async () => {
|
||||||
|
@ -132,6 +145,9 @@ test("create entry execution (update)", async () => {
|
||||||
expect(entry.execution?.id).toBe(x2);
|
expect(entry.execution?.id).toBe(x2);
|
||||||
expect(entry.execution?.text).toBe("x2");
|
expect(entry.execution?.text).toBe("x2");
|
||||||
expect(entry.execution?.author.id).toBe(2);
|
expect(entry.execution?.author.id).toBe(2);
|
||||||
|
|
||||||
|
const nExecutions = await getEntryNExecutions(eId);
|
||||||
|
expect(nExecutions).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create entry execution (wrong old xid)", async () => {
|
test("create entry execution (wrong old xid)", async () => {
|
||||||
|
|
Loading…
Reference in a new issue