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
 | 
			
		||||
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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
    <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
    %sveltekit.head%
 | 
			
		||||
  </head>
 | 
			
		||||
  <body data-sveltekit-preload-data="hover">
 | 
			
		||||
  <body data-sveltekit-preload-data="tap">
 | 
			
		||||
    <div style="display: contents">%sveltekit.body%</div>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@
 | 
			
		|||
  export let placeholder: string | undefined = undefined;
 | 
			
		||||
  export let padding = true;
 | 
			
		||||
  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 idInputName: string | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@
 | 
			
		|||
  export let onUpdate: (filterData: FilterQdata | undefined) => void = () => {};
 | 
			
		||||
  /** List of hidden filter IDs, can be specified for prefiltered views (e.g. by patient) */
 | 
			
		||||
  export let hiddenFilters: string[] = [];
 | 
			
		||||
  /** True if a separate search field should be displayed */
 | 
			
		||||
  export let search = false;
 | 
			
		||||
 | 
			
		||||
  let autocomplete: Autocomplete | undefined;
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +60,9 @@
 | 
			
		|||
  function updateFromQueryData(filterData: FilterQdata) {
 | 
			
		||||
    const filters: 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") {
 | 
			
		||||
        searchVal = value.toString();
 | 
			
		||||
      } else if (Array.isArray(value)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -183,10 +186,8 @@
 | 
			
		|||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-wrap w-full items-start justify-center gap-2 relative">
 | 
			
		||||
  <div
 | 
			
		||||
    class="flex flex-wrap flex-grow items-stretch h-auto p-0 gap-2 input input-sm input-bordered relative"
 | 
			
		||||
  >
 | 
			
		||||
<div class="filterbar-outer">
 | 
			
		||||
  <div class="filterbar-inner input input-sm input-bordered">
 | 
			
		||||
    {#each activeFilters as fdata, i}
 | 
			
		||||
      <EntryFilterChip
 | 
			
		||||
        filter={FILTERS[fdata.id]}
 | 
			
		||||
| 
						 | 
				
			
			@ -221,6 +222,7 @@
 | 
			
		|||
      aria-label="Alle Filter entfernen"
 | 
			
		||||
      on:click={() => {
 | 
			
		||||
        activeFilters = [];
 | 
			
		||||
        searchVal = "";
 | 
			
		||||
        updateFilter();
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
| 
						 | 
				
			
			@ -239,3 +241,14 @@
 | 
			
		|||
  {/if}
 | 
			
		||||
  <slot />
 | 
			
		||||
</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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const TOFF = " aus";
 | 
			
		||||
 | 
			
		||||
  $: toggleState = fdata.selection?.toggle !== false;
 | 
			
		||||
  $: filterName = toggleState ? filter.name : filter.toggleOff?.name;
 | 
			
		||||
  $: filterIcon = toggleState ? filter.icon : filter.toggleOff?.icon;
 | 
			
		||||
  $: filterName = toggleState
 | 
			
		||||
    ? filter.name
 | 
			
		||||
    : filter.toggleOff?.name ?? filter.name + TOFF;
 | 
			
		||||
  $: filterIcon = toggleState ? filter.icon : filter.toggleOff?.icon ?? filter.icon;
 | 
			
		||||
  $: hasInputField = filter.inputType !== 0 && filter.inputType !== 3;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import { trpc } from "$lib/shared/trpc";
 | 
			
		|||
import {
 | 
			
		||||
  mdiAccount,
 | 
			
		||||
  mdiAccountInjury,
 | 
			
		||||
  mdiAccountMultipleOutline,
 | 
			
		||||
  mdiAccountRemoveOutline,
 | 
			
		||||
  mdiBedKingOutline,
 | 
			
		||||
  mdiCheckboxBlankOutline,
 | 
			
		||||
| 
						 | 
				
			
			@ -118,4 +119,10 @@ export const PATIENT_FILTER: { [key: string]: FilterDef } = {
 | 
			
		|||
    icon: mdiAccountRemoveOutline,
 | 
			
		||||
    inputType: 0,
 | 
			
		||||
  },
 | 
			
		||||
  includeHidden: {
 | 
			
		||||
    id: "includeHidden",
 | 
			
		||||
    name: "Alle anzeigen",
 | 
			
		||||
    icon: mdiAccountMultipleOutline,
 | 
			
		||||
    inputType: 0,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@
 | 
			
		|||
  import { superForm, superValidateSync } from "sveltekit-superforms/client";
 | 
			
		||||
  import type { SuperValidated } from "sveltekit-superforms";
 | 
			
		||||
  import { ZPatientNew } from "$lib/shared/model/validation";
 | 
			
		||||
  import { browser } from "$app/environment";
 | 
			
		||||
 | 
			
		||||
  export let patient: RouterOutput["patient"]["get"] | null = null;
 | 
			
		||||
  export let formData: SuperValidated<typeof ZPatientNew> =
 | 
			
		||||
| 
						 | 
				
			
			@ -91,11 +92,14 @@
 | 
			
		|||
    </label>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-wrap gap-2">
 | 
			
		||||
    <button
 | 
			
		||||
      class="btn btn-primary max-w-32"
 | 
			
		||||
      type="submit"
 | 
			
		||||
    disabled={$tainted === undefined}
 | 
			
		||||
      disabled={browser && $tainted === undefined}
 | 
			
		||||
    >
 | 
			
		||||
      Speichern
 | 
			
		||||
    </button>
 | 
			
		||||
    <slot />
 | 
			
		||||
  </div>
 | 
			
		||||
</form>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,11 +4,9 @@
 | 
			
		|||
  import FilterBar from "$lib/components/filter/FilterBar.svelte";
 | 
			
		||||
  import type { PaginationRequest, SortRequest } from "$lib/shared/model";
 | 
			
		||||
  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 { 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 { z } from "zod";
 | 
			
		||||
  import { browser } from "$app/environment";
 | 
			
		||||
| 
						 | 
				
			
			@ -19,9 +17,6 @@
 | 
			
		|||
  export let baseUrl: string;
 | 
			
		||||
  export let patientId: number | null = null;
 | 
			
		||||
 | 
			
		||||
  let loadingBar: LoadingBar | undefined;
 | 
			
		||||
  let loadError: Error | null = null;
 | 
			
		||||
 | 
			
		||||
  function paginationUpdate(pagination: PaginationRequest) {
 | 
			
		||||
    updateQuery({
 | 
			
		||||
      filter: query.filter,
 | 
			
		||||
| 
						 | 
				
			
			@ -49,29 +44,6 @@
 | 
			
		|||
      // Update page URL
 | 
			
		||||
      const url = getQueryUrl(q, baseUrl);
 | 
			
		||||
      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>
 | 
			
		||||
| 
						 | 
				
			
			@ -85,11 +57,6 @@
 | 
			
		|||
  <slot />
 | 
			
		||||
</FilterBar>
 | 
			
		||||
 | 
			
		||||
<LoadingBar bind:this={loadingBar} alwaysShown />
 | 
			
		||||
 | 
			
		||||
{#if loadError}
 | 
			
		||||
  <ErrorMessage error={loadError} />
 | 
			
		||||
{:else}
 | 
			
		||||
<EntryTable
 | 
			
		||||
  {entries}
 | 
			
		||||
  sortData={query.sort}
 | 
			
		||||
| 
						 | 
				
			
			@ -103,4 +70,3 @@
 | 
			
		|||
  data={entries}
 | 
			
		||||
  onUpdate={paginationUpdate}
 | 
			
		||||
/>
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,11 +4,9 @@
 | 
			
		|||
  import FilterBar from "$lib/components/filter/FilterBar.svelte";
 | 
			
		||||
  import type { PaginationRequest, SortRequest } from "$lib/shared/model";
 | 
			
		||||
  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 { 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 { z } from "zod";
 | 
			
		||||
  import { browser } from "$app/environment";
 | 
			
		||||
| 
						 | 
				
			
			@ -18,9 +16,6 @@
 | 
			
		|||
  export let patients: RouterOutput["patient"]["list"];
 | 
			
		||||
  export let baseUrl: string;
 | 
			
		||||
 | 
			
		||||
  let loadingBar: LoadingBar | undefined;
 | 
			
		||||
  let loadError: Error | null = null;
 | 
			
		||||
 | 
			
		||||
  function paginationUpdate(pagination: PaginationRequest) {
 | 
			
		||||
    updateQuery({
 | 
			
		||||
      filter: query.filter,
 | 
			
		||||
| 
						 | 
				
			
			@ -46,22 +41,6 @@
 | 
			
		|||
      // Update page URL
 | 
			
		||||
      const url = getQueryUrl(q, baseUrl);
 | 
			
		||||
      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>
 | 
			
		||||
| 
						 | 
				
			
			@ -75,11 +54,6 @@
 | 
			
		|||
  <slot />
 | 
			
		||||
</FilterBar>
 | 
			
		||||
 | 
			
		||||
<LoadingBar bind:this={loadingBar} alwaysShown />
 | 
			
		||||
 | 
			
		||||
{#if loadError}
 | 
			
		||||
  <ErrorMessage error={loadError} />
 | 
			
		||||
{:else}
 | 
			
		||||
<PatientTable {patients} sortData={query.sort} {sortUpdate} {baseUrl} />
 | 
			
		||||
 | 
			
		||||
<PaginationButtons
 | 
			
		||||
| 
						 | 
				
			
			@ -87,4 +61,3 @@
 | 
			
		|||
  data={patients}
 | 
			
		||||
  onUpdate={paginationUpdate}
 | 
			
		||||
/>
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
  import type { RouterOutput } from "$lib/shared/trpc";
 | 
			
		||||
  import { gotoEntityQuery } from "$lib/shared/util";
 | 
			
		||||
 | 
			
		||||
  export let patient: RouterOutput["patient"]["get"];
 | 
			
		||||
  export let patient: RouterOutput["patient"]["list"]["items"][0];
 | 
			
		||||
  export let baseUrl: string;
 | 
			
		||||
 | 
			
		||||
  function onClick(e: MouseEvent) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,13 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import type { SortRequest } from "$lib/shared/model";
 | 
			
		||||
  import type { RouterOutput } from "$lib/shared/trpc";
 | 
			
		||||
  import { formatDate } from "$lib/shared/util";
 | 
			
		||||
  import { mdiClose } from "@mdi/js";
 | 
			
		||||
  import { formatDate, gotoEntityQuery } from "$lib/shared/util";
 | 
			
		||||
  import { mdiClose, mdiFilter } from "@mdi/js";
 | 
			
		||||
  import Icon from "$lib/components/ui/Icon.svelte";
 | 
			
		||||
 | 
			
		||||
  import RoomField from "./RoomField.svelte";
 | 
			
		||||
  import SortHeader from "./SortHeader.svelte";
 | 
			
		||||
  import { URL_ENTRIES } from "$lib/shared/constants";
 | 
			
		||||
 | 
			
		||||
  export let patients: RouterOutput["patient"]["list"];
 | 
			
		||||
  export let sortData: SortRequest | undefined = undefined;
 | 
			
		||||
| 
						 | 
				
			
			@ -28,9 +29,10 @@
 | 
			
		|||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
      {#each patients.items as patient}
 | 
			
		||||
        {@const full_name = patient.first_name + " " + patient.last_name}
 | 
			
		||||
        <tr
 | 
			
		||||
          class="transition-colors hover:bg-neutral-content/10"
 | 
			
		||||
          class:hidden={patient.hidden}
 | 
			
		||||
          class:p-hidden={patient.hidden}
 | 
			
		||||
        >
 | 
			
		||||
          <td
 | 
			
		||||
            ><a
 | 
			
		||||
| 
						 | 
				
			
			@ -39,18 +41,29 @@
 | 
			
		|||
              aria-label="Eintrag anzeigen">{patient.id}</a
 | 
			
		||||
            ></td
 | 
			
		||||
          >
 | 
			
		||||
          <td>{patient.first_name} {patient.last_name}</td>
 | 
			
		||||
          <td>{patient.age}</td>
 | 
			
		||||
          <td>{full_name}</td>
 | 
			
		||||
          <td>{patient.age ?? ""}</td>
 | 
			
		||||
          <td>
 | 
			
		||||
            {#if patient.room}
 | 
			
		||||
              <RoomField room={patient.room} {baseUrl} />
 | 
			
		||||
            {/if}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td>{formatDate(patient.created_at, true)}</td>
 | 
			
		||||
          <td>
 | 
			
		||||
            <button class="btn btn-circle btn-ghost btn-xs"
 | 
			
		||||
              ><Icon size={1.2} path={mdiClose} /></button
 | 
			
		||||
          <td class="text-right">
 | 
			
		||||
            <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>
 | 
			
		||||
        </tr>
 | 
			
		||||
      {/each}
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +72,7 @@
 | 
			
		|||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="postcss">
 | 
			
		||||
  .hidden {
 | 
			
		||||
  .p-hidden {
 | 
			
		||||
    @apply bg-red-500/20;
 | 
			
		||||
  }
 | 
			
		||||
</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(
 | 
			
		||||
  filter: PatientsFilter = {},
 | 
			
		||||
  pagination: PaginationRequest = {},
 | 
			
		||||
| 
						 | 
				
			
			@ -86,8 +90,10 @@ export async function getPatients(
 | 
			
		|||
    qb.addOrderClause(`similarity(p.full_name, ${qvar}) desc`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (filter.hidden !== undefined) {
 | 
			
		||||
    qb.addFilter("p.hidden", filter.hidden);
 | 
			
		||||
  if (filter.hidden) {
 | 
			
		||||
    qb.addFilter("p.hidden", true);
 | 
			
		||||
  } else if (!filter.includeHidden) {
 | 
			
		||||
    qb.addFilter("p.hidden", false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  qb.addFilterList("r.id", filter.room);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,7 +87,7 @@ class SearchQueryComponents {
 | 
			
		|||
 */
 | 
			
		||||
export function parseSearchQuery(q: string): SearchQueryComponents {
 | 
			
		||||
  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] === "-";
 | 
			
		||||
    // Exact
 | 
			
		||||
    if (m[2]) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import {
 | 
			
		||||
  deletePatient,
 | 
			
		||||
  getPatient,
 | 
			
		||||
  getPatientNEntries,
 | 
			
		||||
  getPatientNames,
 | 
			
		||||
  getPatients,
 | 
			
		||||
  hidePatient,
 | 
			
		||||
| 
						 | 
				
			
			@ -13,9 +14,15 @@ import { z } from "zod";
 | 
			
		|||
 | 
			
		||||
export const patientRouter = t.router({
 | 
			
		||||
  getNames: t.procedure.query(async () => trpcWrap(getPatientNames)),
 | 
			
		||||
  get: t.procedure
 | 
			
		||||
    .input(ZEntityId)
 | 
			
		||||
    .query(async (opts) => trpcWrap(async () => getPatient(opts.input))),
 | 
			
		||||
  get: t.procedure.input(ZEntityId).query(async (opts) =>
 | 
			
		||||
    trpcWrap(async () => {
 | 
			
		||||
      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) => {
 | 
			
		||||
    return getPatients(
 | 
			
		||||
      opts.input.filter ?? {},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,10 +29,14 @@ export type EntriesFilter = Partial<{
 | 
			
		|||
}>;
 | 
			
		||||
 | 
			
		||||
export type PatientsFilter = Partial<{
 | 
			
		||||
  /** Search patient names */
 | 
			
		||||
  search: string;
 | 
			
		||||
  station: FilterList<number>;
 | 
			
		||||
  room: FilterList<number>;
 | 
			
		||||
  /** Show only hidden patients */
 | 
			
		||||
  hidden: boolean;
 | 
			
		||||
  /** Show visible and hidden patients */
 | 
			
		||||
  includeHidden: boolean;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export type TRPCErrorResponse = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -107,6 +107,7 @@ export const ZPatientsFilter = z
 | 
			
		|||
    room: ZFilterList,
 | 
			
		||||
    station: ZFilterList,
 | 
			
		||||
    hidden: z.boolean(),
 | 
			
		||||
    includeHidden: z.boolean(),
 | 
			
		||||
  })
 | 
			
		||||
  .partial();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,15 +8,23 @@ import { superValidate } from "sveltekit-superforms/server";
 | 
			
		|||
export const actions = {
 | 
			
		||||
  default: async (event) => {
 | 
			
		||||
    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 formData = schema.parse(form);
 | 
			
		||||
    const form = await superValidate(event.request, ZPatientNew);
 | 
			
		||||
    const hide = formData.get("hide");
 | 
			
		||||
    const del = formData.get("delete");
 | 
			
		||||
    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) {
 | 
			
		||||
      // Again, return { form } and things will just work.
 | 
			
		||||
        return fail(400, { form });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -28,5 +36,6 @@ export const actions = {
 | 
			
		|||
      );
 | 
			
		||||
 | 
			
		||||
      return { form };
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
} satisfies Actions;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,16 +4,28 @@
 | 
			
		|||
  import PatientForm from "$lib/components/form/PatientForm.svelte";
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
 | 
			
		||||
  $: hasEntries = data.patient.n_entries > 0;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
  <title>Patient #{data.patient.id}</title>
 | 
			
		||||
</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}
 | 
			
		||||
  <h1 class="heading mt-8 mb-4">Einträge</h1>
 | 
			
		||||
{#if hasEntries}
 | 
			
		||||
  <h1 class="heading mt-8 mb-4">Einträge ({data.patient.n_entries})</h1>
 | 
			
		||||
 | 
			
		||||
  <FilteredEntryTable
 | 
			
		||||
    baseUrl="/patient/{data.patient.id}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue