Compare commits
4 commits
5f396ccaf2
...
12dbf7227b
Author | SHA1 | Date | |
---|---|---|---|
12dbf7227b | |||
39b97c6042 | |||
881dc583e3 | |||
dee579ab46 |
18 changed files with 153 additions and 133 deletions
|
@ -33,4 +33,5 @@ EXECUTE PROCEDURE update_entry_tsvec ();
|
||||||
ALTER TABLE patients
|
ALTER TABLE patients
|
||||||
ADD COLUMN full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;
|
ADD COLUMN full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;
|
||||||
|
|
||||||
|
CREATE INDEX entries_tsvec ON entries USING GIN (tsvec);
|
||||||
CREATE INDEX patients_full_name ON patients USING gin (full_name gin_trgm_ops);
|
CREATE INDEX patients_full_name ON patients USING gin (full_name gin_trgm_ops);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="tap">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
export let placeholder: string | undefined = undefined;
|
export let placeholder: string | undefined = undefined;
|
||||||
export let padding = true;
|
export let padding = true;
|
||||||
export let cls = "";
|
export let cls = "";
|
||||||
export let inputCls = "w-full bg-transparent";
|
export let inputCls = "w-full bg-transparent outline-none";
|
||||||
export let asTextInput = false;
|
export let asTextInput = false;
|
||||||
export let idInputName: string | undefined = undefined;
|
export let idInputName: string | undefined = undefined;
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
export let onUpdate: (filterData: FilterQdata | undefined) => void = () => {};
|
export let onUpdate: (filterData: FilterQdata | undefined) => void = () => {};
|
||||||
/** List of hidden filter IDs, can be specified for prefiltered views (e.g. by patient) */
|
/** List of hidden filter IDs, can be specified for prefiltered views (e.g. by patient) */
|
||||||
export let hiddenFilters: string[] = [];
|
export let hiddenFilters: string[] = [];
|
||||||
|
/** True if a separate search field should be displayed */
|
||||||
export let search = false;
|
export let search = false;
|
||||||
|
|
||||||
let autocomplete: Autocomplete | undefined;
|
let autocomplete: Autocomplete | undefined;
|
||||||
|
@ -59,7 +60,9 @@
|
||||||
function updateFromQueryData(filterData: FilterQdata) {
|
function updateFromQueryData(filterData: FilterQdata) {
|
||||||
const filters: FilterData[] = [];
|
const filters: FilterData[] = [];
|
||||||
for (const [id, value] of Object.entries(filterData)) {
|
for (const [id, value] of Object.entries(filterData)) {
|
||||||
if (hiddenFilters.includes(id)) continue;
|
// If filter is hidden or undefined, dont display it
|
||||||
|
if (hiddenFilters.includes(id) || !FILTERS[id]) continue;
|
||||||
|
// Extract search parameter if a separate search field is used
|
||||||
if (search && id === "search") {
|
if (search && id === "search") {
|
||||||
searchVal = value.toString();
|
searchVal = value.toString();
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
|
@ -183,10 +186,8 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap w-full items-start justify-center gap-2 relative">
|
<div class="filterbar-outer">
|
||||||
<div
|
<div class="filterbar-inner input input-sm input-bordered">
|
||||||
class="flex flex-wrap flex-grow items-stretch h-auto p-0 gap-2 input input-sm input-bordered relative"
|
|
||||||
>
|
|
||||||
{#each activeFilters as fdata, i}
|
{#each activeFilters as fdata, i}
|
||||||
<EntryFilterChip
|
<EntryFilterChip
|
||||||
filter={FILTERS[fdata.id]}
|
filter={FILTERS[fdata.id]}
|
||||||
|
@ -221,6 +222,7 @@
|
||||||
aria-label="Alle Filter entfernen"
|
aria-label="Alle Filter entfernen"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
activeFilters = [];
|
activeFilters = [];
|
||||||
|
searchVal = "";
|
||||||
updateFilter();
|
updateFilter();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -239,3 +241,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.filterbar-outer {
|
||||||
|
@apply flex flex-wrap w-full items-start justify-center gap-2 relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterbar-inner {
|
||||||
|
@apply flex flex-wrap flex-grow items-stretch h-min p-0 gap-2 relative;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -32,9 +32,13 @@
|
||||||
autocomplete.open();
|
autocomplete.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TOFF = " aus";
|
||||||
|
|
||||||
$: toggleState = fdata.selection?.toggle !== false;
|
$: toggleState = fdata.selection?.toggle !== false;
|
||||||
$: filterName = toggleState ? filter.name : filter.toggleOff?.name;
|
$: filterName = toggleState
|
||||||
$: filterIcon = toggleState ? filter.icon : filter.toggleOff?.icon;
|
? filter.name
|
||||||
|
: filter.toggleOff?.name ?? filter.name + TOFF;
|
||||||
|
$: filterIcon = toggleState ? filter.icon : filter.toggleOff?.icon ?? filter.icon;
|
||||||
$: hasInputField = filter.inputType !== 0 && filter.inputType !== 3;
|
$: hasInputField = filter.inputType !== 0 && filter.inputType !== 3;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { trpc } from "$lib/shared/trpc";
|
||||||
import {
|
import {
|
||||||
mdiAccount,
|
mdiAccount,
|
||||||
mdiAccountInjury,
|
mdiAccountInjury,
|
||||||
|
mdiAccountMultipleOutline,
|
||||||
mdiAccountRemoveOutline,
|
mdiAccountRemoveOutline,
|
||||||
mdiBedKingOutline,
|
mdiBedKingOutline,
|
||||||
mdiCheckboxBlankOutline,
|
mdiCheckboxBlankOutline,
|
||||||
|
@ -118,4 +119,10 @@ export const PATIENT_FILTER: { [key: string]: FilterDef } = {
|
||||||
icon: mdiAccountRemoveOutline,
|
icon: mdiAccountRemoveOutline,
|
||||||
inputType: 0,
|
inputType: 0,
|
||||||
},
|
},
|
||||||
|
includeHidden: {
|
||||||
|
id: "includeHidden",
|
||||||
|
name: "Alle anzeigen",
|
||||||
|
icon: mdiAccountMultipleOutline,
|
||||||
|
inputType: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { superForm, superValidateSync } from "sveltekit-superforms/client";
|
import { superForm, superValidateSync } from "sveltekit-superforms/client";
|
||||||
import type { SuperValidated } from "sveltekit-superforms";
|
import type { SuperValidated } from "sveltekit-superforms";
|
||||||
import { ZPatientNew } from "$lib/shared/model/validation";
|
import { ZPatientNew } from "$lib/shared/model/validation";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
export let patient: RouterOutput["patient"]["get"] | null = null;
|
export let patient: RouterOutput["patient"]["get"] | null = null;
|
||||||
export let formData: SuperValidated<typeof ZPatientNew> =
|
export let formData: SuperValidated<typeof ZPatientNew> =
|
||||||
|
@ -91,11 +92,14 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div class="flex flex-wrap gap-2">
|
||||||
class="btn btn-primary max-w-32"
|
<button
|
||||||
type="submit"
|
class="btn btn-primary max-w-32"
|
||||||
disabled={$tainted === undefined}
|
type="submit"
|
||||||
>
|
disabled={browser && $tainted === undefined}
|
||||||
Speichern
|
>
|
||||||
</button>
|
Speichern
|
||||||
|
</button>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -4,11 +4,9 @@
|
||||||
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
||||||
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||||
import type { FilterQdata } from "$lib/components/filter/types";
|
import type { FilterQdata } from "$lib/components/filter/types";
|
||||||
import { trpc, type RouterOutput } from "$lib/shared/trpc";
|
import { type RouterOutput } from "$lib/shared/trpc";
|
||||||
import { ENTRY_FILTERS } from "$lib/components/filter/filters";
|
import { ENTRY_FILTERS } from "$lib/components/filter/filters";
|
||||||
import { getQueryUrl } from "$lib/shared/util";
|
import { getQueryUrl } from "$lib/shared/util";
|
||||||
import LoadingBar from "$lib/components/ui/LoadingBar.svelte";
|
|
||||||
import ErrorMessage from "$lib/components/ui/ErrorMessage.svelte";
|
|
||||||
import type { ZEntriesQuery } from "$lib/shared/model/validation";
|
import type { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
@ -19,9 +17,6 @@
|
||||||
export let baseUrl: string;
|
export let baseUrl: string;
|
||||||
export let patientId: number | null = null;
|
export let patientId: number | null = null;
|
||||||
|
|
||||||
let loadingBar: LoadingBar | undefined;
|
|
||||||
let loadError: Error | null = null;
|
|
||||||
|
|
||||||
function paginationUpdate(pagination: PaginationRequest) {
|
function paginationUpdate(pagination: PaginationRequest) {
|
||||||
updateQuery({
|
updateQuery({
|
||||||
filter: query.filter,
|
filter: query.filter,
|
||||||
|
@ -49,29 +44,6 @@
|
||||||
// Update page URL
|
// Update page URL
|
||||||
const url = getQueryUrl(q, baseUrl);
|
const url = getQueryUrl(q, baseUrl);
|
||||||
goto(url, { replaceState: true, keepFocus: true });
|
goto(url, { replaceState: true, keepFocus: true });
|
||||||
|
|
||||||
// Apply patient filter
|
|
||||||
if (patientId !== null) {
|
|
||||||
if (!q.filter) q.filter = {};
|
|
||||||
q.filter.patient = [{ id: patientId }];
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingBar?.start();
|
|
||||||
trpc()
|
|
||||||
.entry.list.query(q)
|
|
||||||
.then((ent) => {
|
|
||||||
entries = ent;
|
|
||||||
loadError = null;
|
|
||||||
loadingBar?.reset();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
query = q;
|
|
||||||
loadError = err;
|
|
||||||
loadingBar?.error();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
query = q;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -85,22 +57,16 @@
|
||||||
<slot />
|
<slot />
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
||||||
<LoadingBar bind:this={loadingBar} alwaysShown />
|
<EntryTable
|
||||||
|
{entries}
|
||||||
|
sortData={query.sort}
|
||||||
|
{sortUpdate}
|
||||||
|
perPatient={patientId !== null}
|
||||||
|
{baseUrl}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if loadError}
|
<PaginationButtons
|
||||||
<ErrorMessage error={loadError} />
|
paginationData={query.pagination}
|
||||||
{:else}
|
data={entries}
|
||||||
<EntryTable
|
onUpdate={paginationUpdate}
|
||||||
{entries}
|
/>
|
||||||
sortData={query.sort}
|
|
||||||
{sortUpdate}
|
|
||||||
perPatient={patientId !== null}
|
|
||||||
{baseUrl}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PaginationButtons
|
|
||||||
paginationData={query.pagination}
|
|
||||||
data={entries}
|
|
||||||
onUpdate={paginationUpdate}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
|
@ -4,11 +4,9 @@
|
||||||
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
||||||
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||||
import type { FilterQdata } from "$lib/components/filter/types";
|
import type { FilterQdata } from "$lib/components/filter/types";
|
||||||
import { trpc, type RouterOutput } from "$lib/shared/trpc";
|
import { type RouterOutput } from "$lib/shared/trpc";
|
||||||
import { PATIENT_FILTER } from "$lib/components/filter/filters";
|
import { PATIENT_FILTER } from "$lib/components/filter/filters";
|
||||||
import { getQueryUrl } from "$lib/shared/util";
|
import { getQueryUrl } from "$lib/shared/util";
|
||||||
import LoadingBar from "$lib/components/ui/LoadingBar.svelte";
|
|
||||||
import ErrorMessage from "$lib/components/ui/ErrorMessage.svelte";
|
|
||||||
import type { ZPatientsQuery } from "$lib/shared/model/validation";
|
import type { ZPatientsQuery } from "$lib/shared/model/validation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
@ -18,9 +16,6 @@
|
||||||
export let patients: RouterOutput["patient"]["list"];
|
export let patients: RouterOutput["patient"]["list"];
|
||||||
export let baseUrl: string;
|
export let baseUrl: string;
|
||||||
|
|
||||||
let loadingBar: LoadingBar | undefined;
|
|
||||||
let loadError: Error | null = null;
|
|
||||||
|
|
||||||
function paginationUpdate(pagination: PaginationRequest) {
|
function paginationUpdate(pagination: PaginationRequest) {
|
||||||
updateQuery({
|
updateQuery({
|
||||||
filter: query.filter,
|
filter: query.filter,
|
||||||
|
@ -46,22 +41,6 @@
|
||||||
// Update page URL
|
// Update page URL
|
||||||
const url = getQueryUrl(q, baseUrl);
|
const url = getQueryUrl(q, baseUrl);
|
||||||
goto(url, { replaceState: true, keepFocus: true });
|
goto(url, { replaceState: true, keepFocus: true });
|
||||||
|
|
||||||
loadingBar?.start();
|
|
||||||
trpc()
|
|
||||||
.patient.list.query(q)
|
|
||||||
.then((p) => {
|
|
||||||
patients = p;
|
|
||||||
loadError = null;
|
|
||||||
loadingBar?.reset();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
loadError = err;
|
|
||||||
loadingBar?.error();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
query = q;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -75,16 +54,10 @@
|
||||||
<slot />
|
<slot />
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
||||||
<LoadingBar bind:this={loadingBar} alwaysShown />
|
<PatientTable {patients} sortData={query.sort} {sortUpdate} {baseUrl} />
|
||||||
|
|
||||||
{#if loadError}
|
<PaginationButtons
|
||||||
<ErrorMessage error={loadError} />
|
paginationData={query.pagination}
|
||||||
{:else}
|
data={patients}
|
||||||
<PatientTable {patients} sortData={query.sort} {sortUpdate} {baseUrl} />
|
onUpdate={paginationUpdate}
|
||||||
|
/>
|
||||||
<PaginationButtons
|
|
||||||
paginationData={query.pagination}
|
|
||||||
data={patients}
|
|
||||||
onUpdate={paginationUpdate}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import type { RouterOutput } from "$lib/shared/trpc";
|
import type { RouterOutput } from "$lib/shared/trpc";
|
||||||
import { gotoEntityQuery } from "$lib/shared/util";
|
import { gotoEntityQuery } from "$lib/shared/util";
|
||||||
|
|
||||||
export let patient: RouterOutput["patient"]["get"];
|
export let patient: RouterOutput["patient"]["list"]["items"][0];
|
||||||
export let baseUrl: string;
|
export let baseUrl: string;
|
||||||
|
|
||||||
function onClick(e: MouseEvent) {
|
function onClick(e: MouseEvent) {
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SortRequest } from "$lib/shared/model";
|
import type { SortRequest } from "$lib/shared/model";
|
||||||
import type { RouterOutput } from "$lib/shared/trpc";
|
import type { RouterOutput } from "$lib/shared/trpc";
|
||||||
import { formatDate } from "$lib/shared/util";
|
import { formatDate, gotoEntityQuery } from "$lib/shared/util";
|
||||||
import { mdiClose } from "@mdi/js";
|
import { mdiClose, mdiFilter } from "@mdi/js";
|
||||||
import Icon from "$lib/components/ui/Icon.svelte";
|
import Icon from "$lib/components/ui/Icon.svelte";
|
||||||
|
|
||||||
import RoomField from "./RoomField.svelte";
|
import RoomField from "./RoomField.svelte";
|
||||||
import SortHeader from "./SortHeader.svelte";
|
import SortHeader from "./SortHeader.svelte";
|
||||||
|
import { URL_ENTRIES } from "$lib/shared/constants";
|
||||||
|
|
||||||
export let patients: RouterOutput["patient"]["list"];
|
export let patients: RouterOutput["patient"]["list"];
|
||||||
export let sortData: SortRequest | undefined = undefined;
|
export let sortData: SortRequest | undefined = undefined;
|
||||||
|
@ -28,9 +29,10 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each patients.items as patient}
|
{#each patients.items as patient}
|
||||||
|
{@const full_name = patient.first_name + " " + patient.last_name}
|
||||||
<tr
|
<tr
|
||||||
class="transition-colors hover:bg-neutral-content/10"
|
class="transition-colors hover:bg-neutral-content/10"
|
||||||
class:hidden={patient.hidden}
|
class:p-hidden={patient.hidden}
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
><a
|
><a
|
||||||
|
@ -39,18 +41,29 @@
|
||||||
aria-label="Eintrag anzeigen">{patient.id}</a
|
aria-label="Eintrag anzeigen">{patient.id}</a
|
||||||
></td
|
></td
|
||||||
>
|
>
|
||||||
<td>{patient.first_name} {patient.last_name}</td>
|
<td>{full_name}</td>
|
||||||
<td>{patient.age}</td>
|
<td>{patient.age ?? ""}</td>
|
||||||
<td>
|
<td>
|
||||||
{#if patient.room}
|
{#if patient.room}
|
||||||
<RoomField room={patient.room} {baseUrl} />
|
<RoomField room={patient.room} {baseUrl} />
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>{formatDate(patient.created_at, true)}</td>
|
<td>{formatDate(patient.created_at, true)}</td>
|
||||||
<td>
|
<td class="text-right">
|
||||||
<button class="btn btn-circle btn-ghost btn-xs"
|
<button
|
||||||
><Icon size={1.2} path={mdiClose} /></button
|
class="btn btn-circle btn-ghost btn-xs inline"
|
||||||
|
on:click={() => {
|
||||||
|
gotoEntityQuery(
|
||||||
|
{ filter: { patient: [{ id: patient.id, name: full_name }] } },
|
||||||
|
URL_ENTRIES
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<Icon size={1.2} path={mdiFilter} />
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-circle btn-ghost btn-xs inline">
|
||||||
|
<Icon size={1.2} path={mdiClose} />
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -59,7 +72,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.hidden {
|
.p-hidden {
|
||||||
@apply bg-red-500/20;
|
@apply bg-red-500/20;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -64,6 +64,10 @@ export async function getPatientNames(): Promise<PatientTag[]> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPatientNEntries(id: number): Promise<number> {
|
||||||
|
return prisma.entry.count({ where: { patient_id: id } });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPatients(
|
export async function getPatients(
|
||||||
filter: PatientsFilter = {},
|
filter: PatientsFilter = {},
|
||||||
pagination: PaginationRequest = {},
|
pagination: PaginationRequest = {},
|
||||||
|
@ -86,8 +90,10 @@ export async function getPatients(
|
||||||
qb.addOrderClause(`similarity(p.full_name, ${qvar}) desc`);
|
qb.addOrderClause(`similarity(p.full_name, ${qvar}) desc`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.hidden !== undefined) {
|
if (filter.hidden) {
|
||||||
qb.addFilter("p.hidden", filter.hidden);
|
qb.addFilter("p.hidden", true);
|
||||||
|
} else if (!filter.includeHidden) {
|
||||||
|
qb.addFilter("p.hidden", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
qb.addFilterList("r.id", filter.room);
|
qb.addFilterList("r.id", filter.room);
|
||||||
|
|
|
@ -87,7 +87,7 @@ class SearchQueryComponents {
|
||||||
*/
|
*/
|
||||||
export function parseSearchQuery(q: string): SearchQueryComponents {
|
export function parseSearchQuery(q: string): SearchQueryComponents {
|
||||||
const regexpParts = /(-)?(?:"([^"]*)"|([^"\s]+))(?:\s|$)/g;
|
const regexpParts = /(-)?(?:"([^"]*)"|([^"\s]+))(?:\s|$)/g;
|
||||||
const components = Array.from(q.matchAll(regexpParts), (m) => {
|
const components = Array.from(q.replaceAll("'", '"').matchAll(regexpParts), (m) => {
|
||||||
const negative = m[1] === "-";
|
const negative = m[1] === "-";
|
||||||
// Exact
|
// Exact
|
||||||
if (m[2]) {
|
if (m[2]) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
deletePatient,
|
deletePatient,
|
||||||
getPatient,
|
getPatient,
|
||||||
|
getPatientNEntries,
|
||||||
getPatientNames,
|
getPatientNames,
|
||||||
getPatients,
|
getPatients,
|
||||||
hidePatient,
|
hidePatient,
|
||||||
|
@ -13,9 +14,15 @@ import { z } from "zod";
|
||||||
|
|
||||||
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
|
get: t.procedure.input(ZEntityId).query(async (opts) =>
|
||||||
.input(ZEntityId)
|
trpcWrap(async () => {
|
||||||
.query(async (opts) => trpcWrap(async () => getPatient(opts.input))),
|
const [patient, n_entries] = await Promise.all([
|
||||||
|
getPatient(opts.input),
|
||||||
|
getPatientNEntries(opts.input),
|
||||||
|
]);
|
||||||
|
return { ...patient, n_entries };
|
||||||
|
})
|
||||||
|
),
|
||||||
list: t.procedure.input(ZPatientsQuery).query(async (opts) => {
|
list: t.procedure.input(ZPatientsQuery).query(async (opts) => {
|
||||||
return getPatients(
|
return getPatients(
|
||||||
opts.input.filter ?? {},
|
opts.input.filter ?? {},
|
||||||
|
|
|
@ -29,10 +29,14 @@ export type EntriesFilter = Partial<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type PatientsFilter = Partial<{
|
export type PatientsFilter = Partial<{
|
||||||
|
/** Search patient names */
|
||||||
search: string;
|
search: string;
|
||||||
station: FilterList<number>;
|
station: FilterList<number>;
|
||||||
room: FilterList<number>;
|
room: FilterList<number>;
|
||||||
|
/** Show only hidden patients */
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
|
/** Show visible and hidden patients */
|
||||||
|
includeHidden: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type TRPCErrorResponse = {
|
export type TRPCErrorResponse = {
|
||||||
|
|
|
@ -107,6 +107,7 @@ export const ZPatientsFilter = z
|
||||||
room: ZFilterList,
|
room: ZFilterList,
|
||||||
station: ZFilterList,
|
station: ZFilterList,
|
||||||
hidden: z.boolean(),
|
hidden: z.boolean(),
|
||||||
|
includeHidden: z.boolean(),
|
||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
|
|
|
@ -8,25 +8,34 @@ import { superValidate } from "sveltekit-superforms/server";
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
const id = ZUrlEntityId.parse(event.params.id);
|
const id = ZUrlEntityId.parse(event.params.id);
|
||||||
// const form = await event.request.formData();
|
const formData = await event.request.formData();
|
||||||
|
|
||||||
// const schema = zfd.formData(ZPatientNew.partial());
|
const hide = formData.get("hide");
|
||||||
// const formData = schema.parse(form);
|
const del = formData.get("delete");
|
||||||
const form = await superValidate(event.request, ZPatientNew);
|
if (hide) {
|
||||||
|
await loadWrap(async () =>
|
||||||
|
trpc(event).patient.hide.mutate({
|
||||||
|
id,
|
||||||
|
hidden: Boolean(parseInt(hide.toString())),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (del) {
|
||||||
|
await loadWrap(async () => trpc(event).patient.delete.mutate(id));
|
||||||
|
} else {
|
||||||
|
const form = await superValidate(formData, ZPatientNew);
|
||||||
|
|
||||||
// Convenient validation check:
|
if (!form.valid) {
|
||||||
if (!form.valid) {
|
return fail(400, { form });
|
||||||
// Again, return { form } and things will just work.
|
}
|
||||||
return fail(400, { form });
|
|
||||||
|
await loadWrap(async () =>
|
||||||
|
trpc(event).patient.update.mutate({
|
||||||
|
id,
|
||||||
|
patient: form.data,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return { form };
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadWrap(async () =>
|
|
||||||
trpc(event).patient.update.mutate({
|
|
||||||
id,
|
|
||||||
patient: form.data,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return { form };
|
|
||||||
},
|
},
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|
|
@ -4,16 +4,28 @@
|
||||||
import PatientForm from "$lib/components/form/PatientForm.svelte";
|
import PatientForm from "$lib/components/form/PatientForm.svelte";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
$: hasEntries = data.patient.n_entries > 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Patient #{data.patient.id}</title>
|
<title>Patient #{data.patient.id}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<PatientForm patient={data.patient} formData={data.form} />
|
<PatientForm patient={data.patient} formData={data.form}>
|
||||||
|
{#if data.patient.hidden}
|
||||||
|
<button type="submit" name="hide" value="0" class="btn btn-primary"
|
||||||
|
>Einblenden</button
|
||||||
|
>
|
||||||
|
{:else if hasEntries}
|
||||||
|
<button type="submit" name="hide" value="1" class="btn">Ausblenden</button>
|
||||||
|
{:else}
|
||||||
|
<button type="submit" name="delete" value="1" class="btn btn-error">Löschen</button>
|
||||||
|
{/if}
|
||||||
|
</PatientForm>
|
||||||
|
|
||||||
{#if data.patient.room}
|
{#if hasEntries}
|
||||||
<h1 class="heading mt-8 mb-4">Einträge</h1>
|
<h1 class="heading mt-8 mb-4">Einträge ({data.patient.n_entries})</h1>
|
||||||
|
|
||||||
<FilteredEntryTable
|
<FilteredEntryTable
|
||||||
baseUrl="/patient/{data.patient.id}"
|
baseUrl="/patient/{data.patient.id}"
|
||||||
|
|
Loading…
Reference in a new issue