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[]
|
accounts Account[]
|
||||||
EntryVersion EntryVersion[]
|
EntryVersion EntryVersion[]
|
||||||
EntryExecution EntryExecution[]
|
EntryExecution EntryExecution[]
|
||||||
|
SavedFilter SavedFilter[]
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
@ -80,8 +81,8 @@ model Patient {
|
||||||
|
|
||||||
full_name String? @default(dbgenerated("((first_name || ' '::text) || last_name)")) @ignore
|
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)
|
@@index([full_name(ops: raw("gin_trgm_ops"))], map: "patients_full_name", type: Gin)
|
||||||
|
@@map("patients")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry category (e.g. Blood test, Exams, ...)
|
// Entry category (e.g. Blood test, Exams, ...)
|
||||||
|
@ -107,8 +108,8 @@ model Entry {
|
||||||
|
|
||||||
tsvec Unsupported("tsvector")?
|
tsvec Unsupported("tsvector")?
|
||||||
|
|
||||||
@@map("entries")
|
|
||||||
@@index([tsvec], type: Gin)
|
@@index([tsvec], type: Gin)
|
||||||
|
@@map("entries")
|
||||||
}
|
}
|
||||||
|
|
||||||
model EntryVersion {
|
model EntryVersion {
|
||||||
|
@ -128,8 +129,8 @@ model EntryVersion {
|
||||||
|
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
|
|
||||||
@@map("entry_versions")
|
|
||||||
@@index([entry_id])
|
@@index([entry_id])
|
||||||
|
@@map("entry_versions")
|
||||||
}
|
}
|
||||||
|
|
||||||
model EntryExecution {
|
model EntryExecution {
|
||||||
|
@ -144,6 +145,19 @@ model EntryExecution {
|
||||||
|
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
|
|
||||||
@@map("entry_executions")
|
|
||||||
@@index([entry_id])
|
@@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 {};
|
export {};
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import Autocomplete from "./Autocomplete.svelte";
|
import Autocomplete from "./Autocomplete.svelte";
|
||||||
import EntryFilterChip from "./FilterChip.svelte";
|
import EntryFilterChip from "./FilterChip.svelte";
|
||||||
|
import SavedFilters from "./SavedFilters.svelte";
|
||||||
import type {
|
import type {
|
||||||
FilterDef,
|
FilterDef,
|
||||||
FilterQdata,
|
FilterQdata,
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
export let hiddenFilters: string[] = [];
|
export let hiddenFilters: string[] = [];
|
||||||
/** True if a separate search field should be displayed */
|
/** True if a separate search field should be displayed */
|
||||||
export let search = false;
|
export let search = false;
|
||||||
|
export let view: string;
|
||||||
|
|
||||||
let autocomplete: Autocomplete<BaseItem> | undefined;
|
let autocomplete: Autocomplete<BaseItem> | undefined;
|
||||||
let activeFilters: FilterData[] = [];
|
let activeFilters: FilterData[] = [];
|
||||||
|
@ -246,6 +248,8 @@
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SavedFilters {view} />
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.filterbar-outer {
|
.filterbar-outer {
|
||||||
@apply flex flex-wrap w-full items-start justify-center gap-2 relative;
|
@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}
|
filterData={query.filter}
|
||||||
hiddenFilters={patientId !== null ? ["patient"] : []}
|
hiddenFilters={patientId !== null ? ["patient"] : []}
|
||||||
onUpdate={filterUpdate}
|
onUpdate={filterUpdate}
|
||||||
|
view="plan"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot name="filterbar" />
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
<EntryTable
|
<EntryTable
|
||||||
{baseUrl}
|
{baseUrl}
|
||||||
{entries}
|
{entries}
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
filterData={query.filter}
|
filterData={query.filter}
|
||||||
onUpdate={filterUpdate}
|
onUpdate={filterUpdate}
|
||||||
search
|
search
|
||||||
|
view="patients"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</FilterBar>
|
</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 { entryRouter } from "./routes/entry";
|
||||||
import { patientRouter } from "./routes/patient";
|
import { patientRouter } from "./routes/patient";
|
||||||
import { roomRouter } from "./routes/room";
|
import { roomRouter } from "./routes/room";
|
||||||
|
import { savedFilterRouter } from "./routes/savedFilter";
|
||||||
import { stationRouter } from "./routes/station";
|
import { stationRouter } from "./routes/station";
|
||||||
import { userRouter } from "./routes/user";
|
import { userRouter } from "./routes/user";
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ export const router = t.router({
|
||||||
room: roomRouter,
|
room: roomRouter,
|
||||||
patient: patientRouter,
|
patient: patientRouter,
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
|
savedFilter: savedFilterRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Router = typeof router;
|
export type Router = typeof router;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { fields, ZCategoryNew } from "$lib/shared/model/validation";
|
import { ZEntityId, ZCategoryNew } from "$lib/shared/model/validation";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteCategory,
|
deleteCategory,
|
||||||
|
@ -12,8 +12,6 @@ import {
|
||||||
|
|
||||||
import { t, trpcWrap } from "..";
|
import { t, trpcWrap } from "..";
|
||||||
|
|
||||||
const ZEntityId = fields.EntityId();
|
|
||||||
|
|
||||||
export const categoryRouter = t.router({
|
export const categoryRouter = t.router({
|
||||||
list: t.procedure.query(async () => trpcWrap(getCategories)),
|
list: t.procedure.query(async () => trpcWrap(getCategories)),
|
||||||
get: t.procedure
|
get: t.procedure
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fields,
|
|
||||||
ZEntriesQuery,
|
ZEntriesQuery,
|
||||||
ZEntryExecutionNew,
|
ZEntryExecutionNew,
|
||||||
ZEntryNew,
|
ZEntryNew,
|
||||||
ZEntryVersionNew,
|
ZEntryVersionNew,
|
||||||
|
ZEntityId,
|
||||||
} from "$lib/shared/model/validation";
|
} from "$lib/shared/model/validation";
|
||||||
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
|
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
|
||||||
|
|
||||||
|
@ -23,8 +23,6 @@ import {
|
||||||
|
|
||||||
import { t, trpcWrap } from "..";
|
import { t, trpcWrap } from "..";
|
||||||
|
|
||||||
const ZEntityId = fields.EntityId();
|
|
||||||
|
|
||||||
export const entryRouter = t.router({
|
export const entryRouter = t.router({
|
||||||
get: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
get: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
||||||
const [entry, n_versions, n_executions] = await Promise.all([
|
const [entry, n_versions, n_executions] = await Promise.all([
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { fields, ZPatientNew, ZPatientsQuery } from "$lib/shared/model/validation";
|
import { ZEntityId, ZPatientNew, ZPatientsQuery } from "$lib/shared/model/validation";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deletePatient,
|
deletePatient,
|
||||||
|
@ -15,8 +15,6 @@ import {
|
||||||
|
|
||||||
import { t, trpcWrap } from "..";
|
import { t, trpcWrap } from "..";
|
||||||
|
|
||||||
const ZEntityId = fields.EntityId();
|
|
||||||
|
|
||||||
export const patientRouter = t.router({
|
export const patientRouter = t.router({
|
||||||
getNames: t.procedure.query(async () => trpcWrap(getPatientNames)),
|
getNames: t.procedure.query(async () => trpcWrap(getPatientNames)),
|
||||||
get: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
get: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { fields, ZRoomNew } from "$lib/shared/model/validation";
|
import { ZEntityId, ZRoomNew } from "$lib/shared/model/validation";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteRoom, getRoom, getRooms, newRoom, updateRoom,
|
deleteRoom, getRoom, getRooms, newRoom, updateRoom,
|
||||||
|
@ -8,8 +8,6 @@ import {
|
||||||
|
|
||||||
import { t, trpcWrap } from "..";
|
import { t, trpcWrap } from "..";
|
||||||
|
|
||||||
const ZEntityId = fields.EntityId();
|
|
||||||
|
|
||||||
export const roomRouter = t.router({
|
export const roomRouter = t.router({
|
||||||
list: t.procedure.query(async (opts) => trpcWrap(getRooms)),
|
list: t.procedure.query(async (opts) => trpcWrap(getRooms)),
|
||||||
get: t.procedure
|
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 { z } from "zod";
|
||||||
|
|
||||||
import { fields, ZStationNew } from "$lib/shared/model/validation";
|
import { ZEntityId, ZStationNew } from "$lib/shared/model/validation";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteStation,
|
deleteStation,
|
||||||
|
@ -12,8 +12,6 @@ import {
|
||||||
|
|
||||||
import { t, trpcWrap } from "..";
|
import { t, trpcWrap } from "..";
|
||||||
|
|
||||||
const ZEntityId = fields.EntityId();
|
|
||||||
|
|
||||||
export const stationRouter = t.router({
|
export const stationRouter = t.router({
|
||||||
list: t.procedure.query(getStations),
|
list: t.procedure.query(getStations),
|
||||||
get: t.procedure
|
get: t.procedure
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { z } from "zod";
|
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 { getUser, getUserNames, getUsers } from "$lib/server/query";
|
||||||
|
|
||||||
import { t, trpcWrap } from "..";
|
import { t, trpcWrap } from "..";
|
||||||
|
|
||||||
const ZEntityId = fields.EntityId();
|
|
||||||
|
|
||||||
export const userRouter = t.router({
|
export const userRouter = t.router({
|
||||||
list: t.procedure
|
list: t.procedure
|
||||||
.input(z.object({ pagination: ZPagination }).partial())
|
.input(z.object({ pagination: ZPagination }).partial())
|
||||||
|
|
|
@ -127,3 +127,11 @@ export type EntryExecution = {
|
||||||
export type EntryExecutionNew = {
|
export type EntryExecutionNew = {
|
||||||
text: string;
|
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 { expect, test } from "vitest";
|
||||||
|
|
||||||
import { fields } from "./validation";
|
import { ZEntriesFilter, fields } from "./validation";
|
||||||
|
|
||||||
test("date string", () => {
|
test("date string", () => {
|
||||||
const DateString = fields.DateString();
|
const DateString = fields.DateString();
|
||||||
|
@ -14,3 +14,17 @@ test("date string", () => {
|
||||||
const dsError = DateString.safeParse("2024-30-10");
|
const dsError = DateString.safeParse("2024-30-10");
|
||||||
expect(dsError.success).toBe(false);
|
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,
|
PaginationRequest,
|
||||||
PatientNew,
|
PatientNew,
|
||||||
RoomNew,
|
RoomNew,
|
||||||
|
SavedFilterNew,
|
||||||
StationNew,
|
StationNew,
|
||||||
User,
|
User,
|
||||||
} from ".";
|
} from ".";
|
||||||
|
@ -40,6 +41,27 @@ export const fields = {
|
||||||
const coercedUint = z.coerce.number().int().nonnegative();
|
const coercedUint = z.coerce.number().int().nonnegative();
|
||||||
const coercedBool = z.string().toLowerCase().transform((v) => v === "true").or(z.boolean());
|
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 ZUrlEntityId = coercedUint;
|
||||||
|
|
||||||
export const ZUser = implement<User>().with({
|
export const ZUser = implement<User>().with({
|
||||||
|
@ -116,7 +138,7 @@ const paginatedQuery = <T extends z.ZodTypeAny>(f: T) => z
|
||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
export const ZEntriesFilter = z
|
export const ZEntriesFilter = returnDataInSameOrderAsPassed(z
|
||||||
.object({
|
.object({
|
||||||
author: ZFilterList,
|
author: ZFilterList,
|
||||||
category: ZFilterList,
|
category: ZFilterList,
|
||||||
|
@ -128,11 +150,11 @@ export const ZEntriesFilter = z
|
||||||
search: z.string(),
|
search: z.string(),
|
||||||
station: ZFilterList,
|
station: ZFilterList,
|
||||||
})
|
})
|
||||||
.partial();
|
.partial());
|
||||||
|
|
||||||
export const ZEntriesQuery = paginatedQuery(ZEntriesFilter);
|
export const ZEntriesQuery = paginatedQuery(ZEntriesFilter);
|
||||||
|
|
||||||
export const ZPatientsFilter = z
|
export const ZPatientsFilter = returnDataInSameOrderAsPassed(z
|
||||||
.object({
|
.object({
|
||||||
search: z.string(),
|
search: z.string(),
|
||||||
room: ZFilterList,
|
room: ZFilterList,
|
||||||
|
@ -140,6 +162,17 @@ export const ZPatientsFilter = z
|
||||||
hidden: coercedBool,
|
hidden: coercedBool,
|
||||||
includeHidden: z.coerce.boolean(),
|
includeHidden: z.coerce.boolean(),
|
||||||
})
|
})
|
||||||
.partial();
|
.partial());
|
||||||
|
|
||||||
export const ZPatientsQuery = paginatedQuery(ZPatientsFilter);
|
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,
|
id: 1,
|
||||||
name: "Laborabnahme",
|
name: "Laborabnahme",
|
||||||
description: "Blutabnahme zur Untersuchung im Labor",
|
description: "Blutabnahme zur Untersuchung im Labor",
|
||||||
color: "FF0000",
|
color: "#FF0000",
|
||||||
},
|
},
|
||||||
created_at: new Date("2024-02-10T11:31:00.000Z"),
|
created_at: new Date("2024-02-10T11:31:00.000Z"),
|
||||||
date: "2024-01-12",
|
date: "2024-01-12",
|
||||||
|
|
|
@ -13,5 +13,5 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<FilteredEntryTable baseUrl={URL_ENTRIES} entries={data.entries} query={data.query}>
|
<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>
|
</FilteredEntryTable>
|
||||||
|
|
Loading…
Reference in a new issue