Compare commits
2 commits
453efb0db7
...
152824f6c0
Author | SHA1 | Date | |
---|---|---|---|
152824f6c0 | |||
99495aaddb |
23 changed files with 312 additions and 66 deletions
13
prisma/migrations/20240425200550_saved_filters/migration.sql
Normal file
13
prisma/migrations/20240425200550_saved_filters/migration.sql
Normal file
|
@ -0,0 +1,13 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "saved_filter" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"view" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"query" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "saved_filter_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "saved_filter" ADD CONSTRAINT "saved_filter_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
@ -40,6 +40,7 @@ model User {
|
|||
accounts Account[]
|
||||
EntryVersion EntryVersion[]
|
||||
EntryExecution EntryExecution[]
|
||||
SavedFilter SavedFilter[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
@ -80,8 +81,8 @@ model Patient {
|
|||
|
||||
full_name String? @default(dbgenerated("((first_name || ' '::text) || last_name)")) @ignore
|
||||
|
||||
@@map("patients")
|
||||
@@index([full_name(ops: raw("gin_trgm_ops"))], map: "patients_full_name", type: Gin)
|
||||
@@map("patients")
|
||||
}
|
||||
|
||||
// Entry category (e.g. Blood test, Exams, ...)
|
||||
|
@ -107,8 +108,8 @@ model Entry {
|
|||
|
||||
tsvec Unsupported("tsvector")?
|
||||
|
||||
@@map("entries")
|
||||
@@index([tsvec], type: Gin)
|
||||
@@map("entries")
|
||||
}
|
||||
|
||||
model EntryVersion {
|
||||
|
@ -128,8 +129,8 @@ model EntryVersion {
|
|||
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("entry_versions")
|
||||
@@index([entry_id])
|
||||
@@map("entry_versions")
|
||||
}
|
||||
|
||||
model EntryExecution {
|
||||
|
@ -144,6 +145,19 @@ model EntryExecution {
|
|||
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("entry_executions")
|
||||
@@index([entry_id])
|
||||
@@map("entry_executions")
|
||||
}
|
||||
|
||||
model SavedFilter {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
user User @relation(fields: [user_id], references: [id])
|
||||
user_id Int
|
||||
|
||||
view String
|
||||
name String
|
||||
query String
|
||||
|
||||
@@map("saved_filter")
|
||||
}
|
||||
|
|
36
src/app.d.ts
vendored
36
src/app.d.ts
vendored
|
@ -20,40 +20,4 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
declare module "@gramex/url/encode" {
|
||||
export default function encode(obj: unknown, settings?: {
|
||||
listBracket?: boolean;
|
||||
listIndex?: boolean;
|
||||
objBracket?: boolean;
|
||||
sortKeys?: boolean;
|
||||
drop?: unknown[];
|
||||
}): string;
|
||||
|
||||
}
|
||||
declare module "@gramex/url/index" {
|
||||
export { default as encode } from "@gramex/url/encode";
|
||||
import { default as update } from "@gramex/url/update";
|
||||
export { update };
|
||||
export const decode: (url: string, settings?: {
|
||||
convert?: boolean;
|
||||
forceList?: boolean;
|
||||
pruneString?: boolean;
|
||||
}) => unknown;
|
||||
|
||||
}
|
||||
declare module "@gramex/url/update" {
|
||||
export default function update(obj: unknown, url: string, settings?: {
|
||||
convert?: boolean;
|
||||
forceList?: boolean;
|
||||
pruneString?: boolean;
|
||||
pruneObject?: boolean;
|
||||
pruneArray?: boolean;
|
||||
}): unknown;
|
||||
|
||||
}
|
||||
declare module "@gramex/url" {
|
||||
import main = require("@gramex/url/index");
|
||||
export = main;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import Autocomplete from "./Autocomplete.svelte";
|
||||
import EntryFilterChip from "./FilterChip.svelte";
|
||||
import SavedFilters from "./SavedFilters.svelte";
|
||||
import type {
|
||||
FilterDef,
|
||||
FilterQdata,
|
||||
|
@ -27,6 +28,7 @@
|
|||
export let hiddenFilters: string[] = [];
|
||||
/** True if a separate search field should be displayed */
|
||||
export let search = false;
|
||||
export let view: string;
|
||||
|
||||
let autocomplete: Autocomplete<BaseItem> | undefined;
|
||||
let activeFilters: FilterData[] = [];
|
||||
|
@ -246,6 +248,8 @@
|
|||
<slot />
|
||||
</div>
|
||||
|
||||
<SavedFilters {view} />
|
||||
|
||||
<style lang="postcss">
|
||||
.filterbar-outer {
|
||||
@apply flex flex-wrap w-full items-start justify-center gap-2 relative;
|
||||
|
|
51
src/lib/components/filter/SavedFilterChip.svelte
Normal file
51
src/lib/components/filter/SavedFilterChip.svelte
Normal file
|
@ -0,0 +1,51 @@
|
|||
<script lang="ts">
|
||||
import { mdiClose, mdiFloppy } from "@mdi/js";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
|
||||
export let href = "";
|
||||
export let onSave = () => {};
|
||||
export let onRemove = () => {};
|
||||
|
||||
function onSaveInt(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
onSave();
|
||||
}
|
||||
|
||||
function onRemoveInt(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
onRemove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<a class="chip btn-transition" {href}>
|
||||
<span class="mx-2">
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<button class="save" on:click={onSaveInt}>
|
||||
<Icon path={mdiFloppy} size={1} />
|
||||
</button>
|
||||
|
||||
<button class="remove" on:click={onRemoveInt}>
|
||||
<Icon path={mdiClose} size={1} />
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<style lang="postcss">
|
||||
.chip {
|
||||
@apply flex items-center overflow-hidden rounded-md bg-primary text-primary-content h-8;
|
||||
|
||||
& > button {
|
||||
@apply border-l border-primary-content/10 px-1 flex h-full items-center;
|
||||
}
|
||||
}
|
||||
|
||||
.save {
|
||||
@apply hover:bg-success/80 active:bg-success focus:bg-success/80;
|
||||
}
|
||||
|
||||
.remove {
|
||||
@apply hover:bg-error/80 active:bg-error focus:bg-error/80;
|
||||
}
|
||||
</style>
|
82
src/lib/components/filter/SavedFilters.svelte
Normal file
82
src/lib/components/filter/SavedFilters.svelte
Normal file
|
@ -0,0 +1,82 @@
|
|||
<!-- Bar of saved filter chips -->
|
||||
<script lang="ts">
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import type { SavedFilter } from "$lib/shared/model";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import LoadingIcon from "$lib/components/ui/LoadingIcon.svelte";
|
||||
|
||||
import Chip from "./SavedFilterChip.svelte";
|
||||
|
||||
export let view: string;
|
||||
|
||||
let filters: SavedFilter[] | null = null;
|
||||
|
||||
onMount(() => {
|
||||
trpc().savedFilter.get.query(view).then((res) => {
|
||||
filters = res;
|
||||
});
|
||||
});
|
||||
|
||||
function getQuery(): string {
|
||||
return window.location.search.substring(1);
|
||||
}
|
||||
|
||||
function create() {
|
||||
const query = getQuery();
|
||||
if (query.length === 0) return;
|
||||
|
||||
const name = prompt("Name");
|
||||
if (!name) return;
|
||||
|
||||
trpc().savedFilter.create.mutate({ name, query, view }).then((id) => {
|
||||
filters?.push({ id, name, query });
|
||||
filters = filters; // force reactive update
|
||||
});
|
||||
}
|
||||
|
||||
function update(ix: number) {
|
||||
const f = filters![ix];
|
||||
const query = getQuery();
|
||||
if (query.length === 0) return;
|
||||
|
||||
trpc().savedFilter.update.mutate({ id: f.id, query });
|
||||
f.query = query;
|
||||
filters = filters; // force reactive update
|
||||
}
|
||||
|
||||
function remove(ix: number) {
|
||||
const f = filters![ix];
|
||||
trpc().savedFilter.delete.mutate(f.id);
|
||||
filters!.splice(ix, 1);
|
||||
filters = filters;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row flex-wrap items-center gap-2">
|
||||
<div class="text-sm h-8 flex items-center">
|
||||
Gespeicherte Filter:
|
||||
</div>
|
||||
|
||||
{#if filters}
|
||||
{#each filters as filter, i (filter.id)}
|
||||
<Chip
|
||||
href={"?" + filter.query}
|
||||
onRemove={() => remove(i)}
|
||||
onSave={() => update(i)}
|
||||
>
|
||||
{filter.name}
|
||||
</Chip>
|
||||
{/each}
|
||||
{:else}
|
||||
<LoadingIcon />
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-sm btn-primary pl-1" on:click={create}>
|
||||
<Icon path={mdiPlus} />
|
||||
Neu
|
||||
</button>
|
||||
</div>
|
|
@ -57,10 +57,13 @@
|
|||
filterData={query.filter}
|
||||
hiddenFilters={patientId !== null ? ["patient"] : []}
|
||||
onUpdate={filterUpdate}
|
||||
view="plan"
|
||||
>
|
||||
<slot />
|
||||
<slot name="filterbar" />
|
||||
</FilterBar>
|
||||
|
||||
<slot />
|
||||
|
||||
<EntryTable
|
||||
{baseUrl}
|
||||
{entries}
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
filterData={query.filter}
|
||||
onUpdate={filterUpdate}
|
||||
search
|
||||
view="patients"
|
||||
>
|
||||
<slot />
|
||||
</FilterBar>
|
||||
|
|
17
src/lib/components/ui/LoadingIcon.svelte
Normal file
17
src/lib/components/ui/LoadingIcon.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { mdiLoading } from "@mdi/js";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
show = false;
|
||||
setTimeout(() => (show = true), 300);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<Icon path={mdiLoading} spin={1} />
|
||||
{/if}
|
28
src/lib/server/query/savedFilter.ts
Normal file
28
src/lib/server/query/savedFilter.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import type { SavedFilter, SavedFilterNew } from "$lib/shared/model";
|
||||
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
export async function newSavedFilter(filter: SavedFilterNew, user_id: number): Promise<number> {
|
||||
const created = await prisma.savedFilter.create({
|
||||
data: {
|
||||
user_id,
|
||||
...filter,
|
||||
},
|
||||
});
|
||||
return created.id;
|
||||
}
|
||||
|
||||
export async function updateSavedFilter(id: number, query: string, user_id: number) {
|
||||
await prisma.savedFilter.update({ where: { id, user_id }, data: { query } });
|
||||
}
|
||||
|
||||
export async function deleteSavedFilter(id: number, user_id: number) {
|
||||
await prisma.savedFilter.delete({ where: { id, user_id } });
|
||||
}
|
||||
|
||||
export async function getSavedFilters(user_id: number, view: string): Promise<SavedFilter[]> {
|
||||
return prisma.savedFilter.findMany({
|
||||
select: { id: true, name: true, query: true },
|
||||
where: { user_id, view },
|
||||
});
|
||||
}
|
|
@ -5,6 +5,7 @@ import { categoryRouter } from "./routes/category";
|
|||
import { entryRouter } from "./routes/entry";
|
||||
import { patientRouter } from "./routes/patient";
|
||||
import { roomRouter } from "./routes/room";
|
||||
import { savedFilterRouter } from "./routes/savedFilter";
|
||||
import { stationRouter } from "./routes/station";
|
||||
import { userRouter } from "./routes/user";
|
||||
|
||||
|
@ -21,6 +22,7 @@ export const router = t.router({
|
|||
room: roomRouter,
|
||||
patient: patientRouter,
|
||||
user: userRouter,
|
||||
savedFilter: savedFilterRouter,
|
||||
});
|
||||
|
||||
export type Router = typeof router;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { fields, ZCategoryNew } from "$lib/shared/model/validation";
|
||||
import { ZEntityId, ZCategoryNew } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deleteCategory,
|
||||
|
@ -12,8 +12,6 @@ import {
|
|||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const categoryRouter = t.router({
|
||||
list: t.procedure.query(async () => trpcWrap(getCategories)),
|
||||
get: t.procedure
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
fields,
|
||||
ZEntriesQuery,
|
||||
ZEntryExecutionNew,
|
||||
ZEntryNew,
|
||||
ZEntryVersionNew,
|
||||
ZEntityId,
|
||||
} from "$lib/shared/model/validation";
|
||||
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
|
||||
|
||||
|
@ -23,8 +23,6 @@ import {
|
|||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const entryRouter = t.router({
|
||||
get: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
||||
const [entry, n_versions, n_executions] = await Promise.all([
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { fields, ZPatientNew, ZPatientsQuery } from "$lib/shared/model/validation";
|
||||
import { ZEntityId, ZPatientNew, ZPatientsQuery } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deletePatient,
|
||||
|
@ -15,8 +15,6 @@ import {
|
|||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const patientRouter = t.router({
|
||||
getNames: t.procedure.query(async () => trpcWrap(getPatientNames)),
|
||||
get: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { fields, ZRoomNew } from "$lib/shared/model/validation";
|
||||
import { ZEntityId, ZRoomNew } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deleteRoom, getRoom, getRooms, newRoom, updateRoom,
|
||||
|
@ -8,8 +8,6 @@ import {
|
|||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const roomRouter = t.router({
|
||||
list: t.procedure.query(async (opts) => trpcWrap(getRooms)),
|
||||
get: t.procedure
|
||||
|
|
24
src/lib/server/trpc/routes/savedFilter.ts
Normal file
24
src/lib/server/trpc/routes/savedFilter.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
ZEntityId, ZSavedFilterNew, ZSavedFilterUpdate, fields,
|
||||
} from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deleteSavedFilter, getSavedFilters, newSavedFilter, updateSavedFilter,
|
||||
} from "$lib/server/query/savedFilter";
|
||||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
export const savedFilterRouter = t.router({
|
||||
get: t.procedure.input(fields.NameString()).query(async (opts) => trpcWrap(
|
||||
async () => getSavedFilters(opts.ctx.user.id, opts.input),
|
||||
)),
|
||||
create: t.procedure.input(ZSavedFilterNew).mutation(async (opts) => trpcWrap(
|
||||
async () => newSavedFilter(opts.input, opts.ctx.user.id),
|
||||
)),
|
||||
update: t.procedure.input(ZSavedFilterUpdate).mutation(async (opts) => trpcWrap(
|
||||
async () => updateSavedFilter(opts.input.id, opts.input.query, opts.ctx.user.id),
|
||||
)),
|
||||
delete: t.procedure.input(ZEntityId).mutation(async (opts) => trpcWrap(
|
||||
async () => deleteSavedFilter(opts.input, opts.ctx.user.id),
|
||||
)),
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { fields, ZStationNew } from "$lib/shared/model/validation";
|
||||
import { ZEntityId, ZStationNew } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deleteStation,
|
||||
|
@ -12,8 +12,6 @@ import {
|
|||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const stationRouter = t.router({
|
||||
list: t.procedure.query(getStations),
|
||||
get: t.procedure
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { fields, ZPagination } from "$lib/shared/model/validation";
|
||||
import { ZEntityId, ZPagination } from "$lib/shared/model/validation";
|
||||
|
||||
import { getUser, getUserNames, getUsers } from "$lib/server/query";
|
||||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const userRouter = t.router({
|
||||
list: t.procedure
|
||||
.input(z.object({ pagination: ZPagination }).partial())
|
||||
|
|
|
@ -127,3 +127,11 @@ export type EntryExecution = {
|
|||
export type EntryExecutionNew = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type SavedFilter = {
|
||||
id: number;
|
||||
name: string;
|
||||
query: string;
|
||||
};
|
||||
|
||||
export type SavedFilterNew = Omit<SavedFilter, "id"> & { view: string };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, test } from "vitest";
|
||||
|
||||
import { fields } from "./validation";
|
||||
import { ZEntriesFilter, fields } from "./validation";
|
||||
|
||||
test("date string", () => {
|
||||
const DateString = fields.DateString();
|
||||
|
@ -14,3 +14,17 @@ test("date string", () => {
|
|||
const dsError = DateString.safeParse("2024-30-10");
|
||||
expect(dsError.success).toBe(false);
|
||||
});
|
||||
|
||||
test("filter data", () => {
|
||||
// Strings from query params should be converted and order kept
|
||||
const entriesFilter = ZEntriesFilter.parse({
|
||||
done: "true",
|
||||
category: [{ id: "1", name: "CA" }],
|
||||
author: [{ id: "1", name: "Max Mustermann" }],
|
||||
});
|
||||
expect(entriesFilter).toStrictEqual({
|
||||
done: true,
|
||||
category: [{ id: 1, name: "CA" }],
|
||||
author: [{ id: 1, name: "Max Mustermann" }],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
PaginationRequest,
|
||||
PatientNew,
|
||||
RoomNew,
|
||||
SavedFilterNew,
|
||||
StationNew,
|
||||
User,
|
||||
} from ".";
|
||||
|
@ -40,6 +41,27 @@ export const fields = {
|
|||
const coercedUint = z.coerce.number().int().nonnegative();
|
||||
const coercedBool = z.string().toLowerCase().transform((v) => v === "true").or(z.boolean());
|
||||
|
||||
function returnDataInSameOrderAsPassed<Schema extends z.ZodObject<z.ZodRawShape>>(
|
||||
schema: Schema,
|
||||
) {
|
||||
return z.any().transform((value, ctx) => {
|
||||
const parsed = schema.safeParse(value);
|
||||
if (parsed.success) {
|
||||
const res: z.infer<Schema> = {};
|
||||
for (const key of Object.keys(value)) {
|
||||
if (key in parsed.data) {
|
||||
// @ts-expect-error keys are safe
|
||||
res[key] = parsed.data[key];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
parsed.error.issues.forEach((iss) => ctx.addIssue(iss));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const ZEntityId = fields.EntityId();
|
||||
export const ZUrlEntityId = coercedUint;
|
||||
|
||||
export const ZUser = implement<User>().with({
|
||||
|
@ -116,7 +138,7 @@ const paginatedQuery = <T extends z.ZodTypeAny>(f: T) => z
|
|||
})
|
||||
.partial();
|
||||
|
||||
export const ZEntriesFilter = z
|
||||
export const ZEntriesFilter = returnDataInSameOrderAsPassed(z
|
||||
.object({
|
||||
author: ZFilterList,
|
||||
category: ZFilterList,
|
||||
|
@ -128,11 +150,11 @@ export const ZEntriesFilter = z
|
|||
search: z.string(),
|
||||
station: ZFilterList,
|
||||
})
|
||||
.partial();
|
||||
.partial());
|
||||
|
||||
export const ZEntriesQuery = paginatedQuery(ZEntriesFilter);
|
||||
|
||||
export const ZPatientsFilter = z
|
||||
export const ZPatientsFilter = returnDataInSameOrderAsPassed(z
|
||||
.object({
|
||||
search: z.string(),
|
||||
room: ZFilterList,
|
||||
|
@ -140,6 +162,17 @@ export const ZPatientsFilter = z
|
|||
hidden: coercedBool,
|
||||
includeHidden: z.coerce.boolean(),
|
||||
})
|
||||
.partial();
|
||||
.partial());
|
||||
|
||||
export const ZPatientsQuery = paginatedQuery(ZPatientsFilter);
|
||||
|
||||
export const ZSavedFilterNew = implement<SavedFilterNew>().with({
|
||||
view: fields.NameString(),
|
||||
name: fields.NameString(),
|
||||
query: z.string(),
|
||||
});
|
||||
|
||||
export const ZSavedFilterUpdate = z.object({
|
||||
id: ZEntityId,
|
||||
query: z.string(),
|
||||
});
|
||||
|
|
|
@ -50,7 +50,7 @@ test("versions diff", () => {
|
|||
id: 1,
|
||||
name: "Laborabnahme",
|
||||
description: "Blutabnahme zur Untersuchung im Labor",
|
||||
color: "FF0000",
|
||||
color: "#FF0000",
|
||||
},
|
||||
created_at: new Date("2024-02-10T11:31:00.000Z"),
|
||||
date: "2024-01-12",
|
||||
|
|
|
@ -13,5 +13,5 @@
|
|||
</svelte:head>
|
||||
|
||||
<FilteredEntryTable baseUrl={URL_ENTRIES} entries={data.entries} query={data.query}>
|
||||
<a class="btn btn-sm btn-primary" href="/entry/new">Neuer Eintrag</a>
|
||||
<a slot="filterbar" class="btn btn-sm btn-primary" href="/entry/new">Neuer Eintrag</a>
|
||||
</FilteredEntryTable>
|
||||
|
|
Loading…
Reference in a new issue