Compare commits
No commits in common. "25a60d5d6120ed94128d85d17737c2291d226a14" and "6c338e447e886f45f371103a10e0ae8c94199c76" have entirely different histories.
25a60d5d61
...
6c338e447e
28 changed files with 119 additions and 464 deletions
|
@ -335,6 +335,10 @@ export default [
|
||||||
// disallow use of the Array constructor
|
// disallow use of the Array constructor
|
||||||
"@typescript-eslint/no-array-constructor": "warn",
|
"@typescript-eslint/no-array-constructor": "warn",
|
||||||
|
|
||||||
|
// disallow if as the only statement in an else block
|
||||||
|
// https://eslint.org/docs/rules/no-lonely-if
|
||||||
|
"no-lonely-if": "warn",
|
||||||
|
|
||||||
// disallow un-paren'd mixes of different operators
|
// disallow un-paren'd mixes of different operators
|
||||||
// https://eslint.org/docs/rules/no-mixed-operators
|
// https://eslint.org/docs/rules/no-mixed-operators
|
||||||
"@stylistic/no-mixed-operators": [
|
"@stylistic/no-mixed-operators": [
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24"><defs><pattern xlink:href="#a" id="b" patternTransform="matrix(-1.39935 -.80792 -4.72493 8.18382 0 0)"/><pattern id="a" width="2" height="1" patternTransform="scale(10)" patternUnits="userSpaceOnUse"><path d="M0-.5h1v2H0z" style="fill:#1a1a1a;stroke:none"/></pattern></defs><path d="M18 22a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2z" style="fill:#565656;fill-opacity:1;fill-rule:nonzero"/><path d="M18 22a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2z" style="fill:url(#b);fill-opacity:1;fill-rule:nonzero"/><path d="M12 2v7L9.5 7.5 7 9V2Z" style="display:inline;fill:#7480ff;fill-opacity:1"/><g style="font-size:13.3333px;line-height:1.25"><path d="M10.53 9.951H8.603l3.414 9.697h2.168l3.419-9.697h-1.932l-2.519 7.633h-.1z" style="fill:#fff"/></g></svg>
|
|
Before Width: | Height: | Size: 895 B |
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB |
|
@ -1,83 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
version="1.1"
|
|
||||||
id="svg4"
|
|
||||||
sodipodi:docname="book.svg"
|
|
||||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs8">
|
|
||||||
<pattern
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#Strips1_1"
|
|
||||||
id="pattern6415"
|
|
||||||
patternTransform="matrix(-1.3993505,-0.80791526,-4.7249348,8.1838246,0,0)" />
|
|
||||||
<pattern
|
|
||||||
inkscape:collect="always"
|
|
||||||
patternUnits="userSpaceOnUse"
|
|
||||||
width="2"
|
|
||||||
height="1"
|
|
||||||
patternTransform="translate(0,0) scale(10,10)"
|
|
||||||
id="Strips1_1"
|
|
||||||
inkscape:stockid="Stripes 1:1"
|
|
||||||
inkscape:isstock="true">
|
|
||||||
<rect
|
|
||||||
style="fill:#1a1a1a;stroke:none"
|
|
||||||
x="0"
|
|
||||||
y="-0.5"
|
|
||||||
width="1"
|
|
||||||
height="2"
|
|
||||||
id="rect2363" />
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview6"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="28.114784"
|
|
||||||
inkscape:cx="15.045465"
|
|
||||||
inkscape:cy="6.4734625"
|
|
||||||
inkscape:window-width="2516"
|
|
||||||
inkscape:window-height="1051"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg4" />
|
|
||||||
<path
|
|
||||||
d="m 18,22 c 1.104569,0 2,-0.895431 2,-2 V 4 C 20,2.89 19.1,2 18,2 H 6 C 4.8954305,2 4,2.8954305 4,4 v 16 c 0,1.104569 0.8954305,2 2,2 z"
|
|
||||||
id="path6417"
|
|
||||||
sodipodi:nodetypes="sssssssss"
|
|
||||||
style="fill:#565656;fill-opacity:1;fill-rule:nonzero"
|
|
||||||
inkscape:label="bookbg" />
|
|
||||||
<path
|
|
||||||
d="m 18,22 c 1.104569,0 2,-0.895431 2,-2 V 4 C 20,2.89 19.1,2 18,2 H 6 C 4.8954305,2 4,2.8954305 4,4 v 16 c 0,1.104569 0.8954305,2 2,2 z"
|
|
||||||
id="path2"
|
|
||||||
sodipodi:nodetypes="sssssssss"
|
|
||||||
style="fill:url(#pattern6415);fill-opacity:1;fill-rule:nonzero"
|
|
||||||
inkscape:label="bookpattern" />
|
|
||||||
<path
|
|
||||||
d="M 12,2 V 9 L 9.5,7.5 7,9 V 2 Z"
|
|
||||||
id="path2-0"
|
|
||||||
inkscape:label="tab"
|
|
||||||
sodipodi:nodetypes="cccccc"
|
|
||||||
style="display:inline;fill:#7480ff;fill-opacity:1" />
|
|
||||||
<g
|
|
||||||
id="text291"
|
|
||||||
style="font-size:13.3333px;line-height:1.25">
|
|
||||||
<path
|
|
||||||
d="M 10.529934,9.9511185 H 8.6028554 l 3.4138176,9.6969455 h 2.168555 L 17.60378,9.9511185 h -1.931814 l -2.518933,7.6325565 h -0.09943 z"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
id="path1804" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.7 KiB |
|
@ -13,7 +13,7 @@
|
||||||
import IconButton from "$lib/components/ui/IconButton.svelte";
|
import IconButton from "$lib/components/ui/IconButton.svelte";
|
||||||
import outclick from "$lib/actions/outclick";
|
import outclick from "$lib/actions/outclick";
|
||||||
|
|
||||||
import type { BaseItem, OnSelectResult } from "./types";
|
import type { BaseItem } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a simplified version of simple-svelte-autocomplete
|
* This is a simplified version of simple-svelte-autocomplete
|
||||||
|
@ -23,6 +23,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type T = $$Generic<BaseItem>;
|
type T = $$Generic<BaseItem>;
|
||||||
|
type OnSelectResult = { newValue: string; close: boolean };
|
||||||
|
|
||||||
/** List of items to choose from (or an async function fetching them) */
|
/** List of items to choose from (or an async function fetching them) */
|
||||||
export let items: T[] | (() => Promise<T[]>);
|
export let items: T[] | (() => Promise<T[]>);
|
||||||
|
@ -57,7 +58,6 @@
|
||||||
export let filterFn: (item: T) => boolean = () => true;
|
export let filterFn: (item: T) => boolean = () => true;
|
||||||
|
|
||||||
/** Selection callback. Returns the new input value after selection */
|
/** Selection callback. Returns the new input value after selection */
|
||||||
export let onTextInput: (value: string) => OnSelectResult | void = () => {};
|
|
||||||
export let onSelect: (item: T, kb: boolean) => OnSelectResult | void = () => {};
|
export let onSelect: (item: T, kb: boolean) => OnSelectResult | void = () => {};
|
||||||
export let onUnselect = (): void => {};
|
export let onUnselect = (): void => {};
|
||||||
export let onClose = (kb: boolean): void => {};
|
export let onClose = (kb: boolean): void => {};
|
||||||
|
@ -267,13 +267,6 @@
|
||||||
function selectItem(): void {
|
function selectItem(): void {
|
||||||
const listItem = filteredItems[highlightIndex];
|
const listItem = filteredItems[highlightIndex];
|
||||||
selectListItem(listItem, true);
|
selectListItem(listItem, true);
|
||||||
if (!listItem && inputElm && inputElm.value.length > 0) {
|
|
||||||
const res = onTextInput(inputElm.value);
|
|
||||||
if (res) {
|
|
||||||
setInputValue(res.newValue);
|
|
||||||
if (res.close) close(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [floatingRef, floatingContent] = createFloatingActions({
|
const [floatingRef, floatingContent] = createFloatingActions({
|
||||||
|
|
|
@ -33,14 +33,6 @@
|
||||||
else onRemove();
|
else onRemove();
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTextInput = filter.textToItem ? (text: string) => {
|
|
||||||
const res = filter.textToItem!(text);
|
|
||||||
if (res) {
|
|
||||||
fdata.selection = res;
|
|
||||||
return { close: true, newValue: "" };
|
|
||||||
}
|
|
||||||
} : undefined;
|
|
||||||
|
|
||||||
$: if (fdata.editing && autocomplete) {
|
$: if (fdata.editing && autocomplete) {
|
||||||
autocomplete.open();
|
autocomplete.open();
|
||||||
}
|
}
|
||||||
|
@ -99,7 +91,6 @@
|
||||||
}
|
}
|
||||||
return { close: false, newValue: item.name ?? "" };
|
return { close: false, newValue: item.name ?? "" };
|
||||||
}}
|
}}
|
||||||
{onTextInput}
|
|
||||||
padding={false}
|
padding={false}
|
||||||
partOfFilterbar
|
partOfFilterbar
|
||||||
selection={fdata.selection?.id
|
selection={fdata.selection?.id
|
||||||
|
|
|
@ -28,7 +28,7 @@ export function weekFilterItems(): BaseItem[] {
|
||||||
res.push({ id: r.toString(), name: r.format() });
|
res.push({ id: r.toString(), name: r.format() });
|
||||||
};
|
};
|
||||||
|
|
||||||
addRange(new DateRange(null, range.end));
|
addRange(new DateRange(null, range.start));
|
||||||
for (let i = 0; i < WEEK_LIMIT; i++) {
|
for (let i = 0; i < WEEK_LIMIT; i++) {
|
||||||
addRange(range);
|
addRange(range);
|
||||||
range.addDays(7);
|
range.addDays(7);
|
||||||
|
@ -104,16 +104,10 @@ export const ENTRY_FILTERS: Record<string, FilterDef> = {
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
id: "date",
|
id: "date",
|
||||||
name: "Datum",
|
name: "Woche",
|
||||||
icon: mdiCalendar,
|
icon: mdiCalendar,
|
||||||
inputType: InputType.FilterList,
|
inputType: InputType.FilterList,
|
||||||
options: async () => weekFilterItems(),
|
options: async () => weekFilterItems(),
|
||||||
textToItem: (s) => {
|
|
||||||
const parsed = DateRange.parseHuman(s);
|
|
||||||
if (parsed) {
|
|
||||||
return { id: parsed.toString(), name: parsed.format() };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ export type FilterDef = {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
};
|
};
|
||||||
options?: () => Promise<BaseItem[]>;
|
options?: () => Promise<BaseItem[]>;
|
||||||
textToItem?: (s: string) => BaseItem | void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FilterData = {
|
export type FilterData = {
|
||||||
|
@ -42,8 +41,6 @@ export type FilterQdata = Record<
|
||||||
string | number | boolean | { id: string | number; name?: string }[]
|
string | number | boolean | { id: string | number; name?: string }[]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type OnSelectResult = { newValue: string; close: boolean };
|
|
||||||
|
|
||||||
export function isFilterValueless(inputType: InputType): boolean {
|
export function isFilterValueless(inputType: InputType): boolean {
|
||||||
return inputType === InputType.None || inputType === InputType.Boolean;
|
return inputType === InputType.None || inputType === InputType.Boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,6 @@
|
||||||
{#if entry.current_version.priority}
|
{#if entry.current_version.priority}
|
||||||
<div class="badge ellipsis badge-warning">Priorität</div>
|
<div class="badge ellipsis badge-warning">Priorität</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if entry.execution && !entry.execution.done}
|
|
||||||
<div class="badge ellipsis badge-warning">Notiz</div>
|
|
||||||
{/if}
|
|
||||||
{#if entry.patient.room}
|
|
||||||
<div class="badge ellipsis badge-primary">{entry.patient.room.name}</div>
|
|
||||||
{/if}
|
|
||||||
<span>{formatPatientName(entry.patient)}</span>
|
<span>{formatPatientName(entry.patient)}</span>
|
||||||
|
|
||||||
<span class="ml-auto hidden md:block">von {entry.current_version.author.name}</span>
|
<span class="ml-auto hidden md:block">von {entry.current_version.author.name}</span>
|
||||||
|
|
|
@ -4,21 +4,14 @@
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
|
|
||||||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
||||||
import type { PaginationRequest } from "$lib/shared/model";
|
import type { Pagination, PaginationRequest } from "$lib/shared/model";
|
||||||
|
|
||||||
import { screenWidthSmall } from "$lib/stores";
|
import { screenWidthSmall } from "$lib/stores";
|
||||||
|
|
||||||
import Icon from "./Icon.svelte";
|
import Icon from "./Icon.svelte";
|
||||||
|
|
||||||
type PaginationX = {
|
|
||||||
items?: unknown[];
|
|
||||||
nItems?: number;
|
|
||||||
total: number;
|
|
||||||
offset: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export let paginationData: PaginationRequest | null | undefined;
|
export let paginationData: PaginationRequest | null | undefined;
|
||||||
export let data: PaginationX;
|
export let data: Pagination<unknown>;
|
||||||
export let onUpdate: (pagination: PaginationRequest) => void = () => {};
|
export let onUpdate: (pagination: PaginationRequest) => void = () => {};
|
||||||
|
|
||||||
$: limit = paginationData?.limit ?? PAGINATION_LIMIT;
|
$: limit = paginationData?.limit ?? PAGINATION_LIMIT;
|
||||||
|
@ -59,7 +52,7 @@
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 items-center">
|
<div class="flex flex-col gap-2 items-center">
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
{data.offset + 1}-{data.offset + (data.items?.length ?? data.nItems ?? 0)} von {data.total}
|
{data.offset + 1}-{data.offset + data.items.length} von {data.total}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="join">
|
<div class="join">
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
|
||||||
mdiChevronDoubleLeft, mdiChevronDoubleRight, mdiChevronLeft, mdiChevronRight,
|
|
||||||
} from "@mdi/js";
|
|
||||||
|
|
||||||
import { DateRange, shiftDateRange } from "$lib/shared/util";
|
import { DateRange } from "$lib/shared/util";
|
||||||
|
|
||||||
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
||||||
import { weekFilterItems } from "$lib/components/filter/filters";
|
import { weekFilterItems } from "$lib/components/filter/filters";
|
||||||
import type { BaseItem, OnSelectResult } from "$lib/components/filter/types";
|
import type { BaseItem } from "$lib/components/filter/types";
|
||||||
|
|
||||||
import IconButton from "./IconButton.svelte";
|
import IconButton from "./IconButton.svelte";
|
||||||
|
|
||||||
|
@ -17,8 +15,26 @@
|
||||||
export let dateRange: DateRange = new DateRange(null, DateRange.thisWeek().end);
|
export let dateRange: DateRange = new DateRange(null, DateRange.thisWeek().end);
|
||||||
export let onSelect: (value: DateRange) => void = () => {};
|
export let onSelect: (value: DateRange) => void = () => {};
|
||||||
|
|
||||||
function addDays(week: boolean, fwd: boolean): void {
|
function addDays(n: number): void {
|
||||||
shiftDateRange(dateRange, week, fwd);
|
if (dateRange.start === null) {
|
||||||
|
dateRange.start = new Date(dateRange.end!);
|
||||||
|
dateRange.start.setDate(dateRange.start.getDate() - 6);
|
||||||
|
} else if (dateRange.end === null) {
|
||||||
|
dateRange.end = new Date(dateRange.start!);
|
||||||
|
dateRange.end.setDate(dateRange.end.getDate() + 6);
|
||||||
|
} else {
|
||||||
|
dateRange.addDays(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextWeek(): void {
|
||||||
|
addDays(7);
|
||||||
|
dateRange = dateRange; // update reactive
|
||||||
|
onSelect(dateRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousWeek(): void {
|
||||||
|
addDays(-7);
|
||||||
dateRange = dateRange; // update reactive
|
dateRange = dateRange; // update reactive
|
||||||
onSelect(dateRange);
|
onSelect(dateRange);
|
||||||
}
|
}
|
||||||
|
@ -31,30 +47,17 @@
|
||||||
editing = false;
|
editing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTextInput(txt: string): OnSelectResult | void {
|
|
||||||
const parsed = DateRange.parseHuman(txt);
|
|
||||||
if (parsed) {
|
|
||||||
dateRange = parsed;
|
|
||||||
onSelect(dateRange);
|
|
||||||
return { close: true, newValue: "" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (editing && autocomplete) {
|
$: if (editing && autocomplete) {
|
||||||
autocomplete.open();
|
autocomplete.open();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<IconButton path={mdiChevronDoubleLeft} on:click={() => addDays(true, false)} />
|
<IconButton path={mdiChevronLeft} on:click={previousWeek} />
|
||||||
<IconButton path={mdiChevronLeft} on:click={() => addDays(false, false)} />
|
|
||||||
|
|
||||||
<div class="w-48">
|
|
||||||
{#if editing}
|
{#if editing}
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
bind:this={autocomplete}
|
bind:this={autocomplete}
|
||||||
items={async () => weekFilterItems()}
|
items={async () => weekFilterItems()}
|
||||||
noAutoselect1
|
|
||||||
onClose={stopEditing}
|
onClose={stopEditing}
|
||||||
onSelect={(item) => {
|
onSelect={(item) => {
|
||||||
if (typeof item.id === "string") {
|
if (typeof item.id === "string") {
|
||||||
|
@ -64,14 +67,10 @@
|
||||||
}
|
}
|
||||||
return { close: true, newValue: "" };
|
return { close: true, newValue: "" };
|
||||||
}}
|
}}
|
||||||
{onTextInput}
|
|
||||||
partOfFilterbar
|
partOfFilterbar
|
||||||
selection={{ id: dateRange.toString(), name: dateRange.format() }} />
|
selection={{ id: dateRange.toString(), name: dateRange.format() }} />
|
||||||
{:else}
|
{:else}
|
||||||
<button class="w-full text-center" on:click={startEditing}>{dateRange.format()}</button>
|
<button on:click={startEditing}>{dateRange.format()}</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
<IconButton path={mdiChevronRight} on:click={nextWeek} />
|
||||||
|
|
||||||
<IconButton path={mdiChevronRight} on:click={() => addDays(false, true)} />
|
|
||||||
<IconButton path={mdiChevronDoubleRight} on:click={() => addDays(true, true)} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -153,9 +153,7 @@ export const ZEntriesFilter = returnDataInSameOrderAsPassed(z
|
||||||
})
|
})
|
||||||
.partial());
|
.partial());
|
||||||
|
|
||||||
export const ZEntriesQuery = paginatedQuery(ZEntriesFilter).extend({
|
export const ZEntriesQuery = paginatedQuery(ZEntriesFilter);
|
||||||
group: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZPatientsFilter = returnDataInSameOrderAsPassed(z
|
export const ZPatientsFilter = returnDataInSameOrderAsPassed(z
|
||||||
.object({
|
.object({
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
import { expect, it, vi } from "vitest";
|
import { expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DateRange,
|
DateRange, dateFromYMD, dateToYMD, formatDate, humanDate, utcDateToYMD,
|
||||||
dateFromHuman,
|
|
||||||
dateFromYMD,
|
|
||||||
dateToYMD,
|
|
||||||
formatDate,
|
|
||||||
humanDate,
|
|
||||||
shiftDateRange,
|
|
||||||
utcDateToYMD,
|
|
||||||
} from "./date";
|
} from "./date";
|
||||||
|
|
||||||
const MINUTE = 60000;
|
const MINUTE = 60000;
|
||||||
|
@ -79,64 +72,3 @@ it.each([
|
||||||
expect(res.toString()).toBe(s);
|
expect(res.toString()).toBe(s);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
|
||||||
// Open ranges
|
|
||||||
{
|
|
||||||
r: "..2024-04-14", week: true, fwd: true, exp: "2024-04-15..2024-04-21",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
r: "..2024-04-14", week: true, fwd: false, exp: "2024-04-08..2024-04-14",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
r: "2024-04-08..", week: true, fwd: true, exp: "2024-04-08..2024-04-14",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
r: "2024-04-08..", week: true, fwd: false, exp: "2024-04-01..2024-04-07",
|
|
||||||
},
|
|
||||||
// Full week
|
|
||||||
{
|
|
||||||
r: "2024-04-08..2024-04-14", week: true, fwd: true, exp: "2024-04-15..2024-04-21",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
r: "2024-04-08..2024-04-14", week: true, fwd: false, exp: "2024-04-01..2024-04-07",
|
|
||||||
},
|
|
||||||
// Partial week
|
|
||||||
{
|
|
||||||
r: "2024-04-13..2024-04-16", week: true, fwd: true, exp: "2024-04-15..2024-04-21",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
r: "2024-04-13..2024-04-16", week: true, fwd: false, exp: "2024-04-08..2024-04-14",
|
|
||||||
},
|
|
||||||
|
|
||||||
// Days
|
|
||||||
{
|
|
||||||
r: "2024-04-13..2024-04-13", week: false, fwd: true, exp: "2024-04-14..2024-04-14",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
r: "2024-04-13..2024-04-13", week: false, fwd: false, exp: "2024-04-12..2024-04-12",
|
|
||||||
},
|
|
||||||
// Full range to day
|
|
||||||
{
|
|
||||||
r: "2024-04-08..2024-04-14", week: false, fwd: true, exp: "2024-04-14..2024-04-14", // TODO: inc date (15)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
r: "2024-04-08..2024-04-14", week: false, fwd: false, exp: "2024-04-08..2024-04-08", // TODO: dec date (7)
|
|
||||||
},
|
|
||||||
// Open range to day
|
|
||||||
{
|
|
||||||
r: "..2024-04-14", week: false, fwd: true, exp: "2024-04-15..2024-04-15",
|
|
||||||
},
|
|
||||||
])("shiftDateRange $r", ({
|
|
||||||
r, week, fwd, exp,
|
|
||||||
}) => {
|
|
||||||
const range = DateRange.parse(r, true)!;
|
|
||||||
const expected = DateRange.parse(exp, true);
|
|
||||||
shiftDateRange(range, week, fwd);
|
|
||||||
expect(range).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("dateFromHuman", () => {
|
|
||||||
expect(dateFromHuman("04.12.2023")).toStrictEqual(new Date(2023, 11, 4));
|
|
||||||
expect(dateFromHuman("04.12.")).toStrictEqual(new Date(new Date().getFullYear(), 11, 4));
|
|
||||||
});
|
|
||||||
|
|
|
@ -42,22 +42,6 @@ export function dateFromYMD(s: string): Date {
|
||||||
return NaN;
|
return NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dateFromHuman(s: string): Date {
|
|
||||||
const parts = s.split(".").filter((x) => x.length > 0).map((x) => parseInt(x));
|
|
||||||
if (parts.length > 0 && parts.length < 4) {
|
|
||||||
const [d, m, y] = parts;
|
|
||||||
const now = new Date();
|
|
||||||
return new Date(
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
y ? y < 1000 ? y + 2000 : y : now.getFullYear(),
|
|
||||||
m ? m - 1 : now.getMonth(),
|
|
||||||
d,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// @ts-expect-error emulate behavior of date constructor
|
|
||||||
return NaN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convert the given date to a string (using the internal UTC format) */
|
/** Convert the given date to a string (using the internal UTC format) */
|
||||||
export function utcDateToYMD(date: Date): string {
|
export function utcDateToYMD(date: Date): string {
|
||||||
return date.toISOString().slice(0, 10);
|
return date.toISOString().slice(0, 10);
|
||||||
|
@ -77,25 +61,19 @@ function dateDiffInDays(a: Date, b: Date): number {
|
||||||
return Math.round((ts2 - ts1) / MS_PER_DAY);
|
return Math.round((ts2 - ts1) / MS_PER_DAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function humanDate(date: Date | string, time = false, cap = false): string {
|
export function humanDate(date: Date | string, time = false): string {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const dt = coerceDate(date);
|
const dt = coerceDate(date);
|
||||||
const threshold = 302400000; // 3.5 * 24 * 3_600_000
|
const threshold = 302400000; // 3.5 * 24 * 3_600_000
|
||||||
const diff = Number(dt) - Number(now); // pos: Future, neg: Past
|
const diff = Number(dt) - Number(now); // pos: Future, neg: Past
|
||||||
if (Math.abs(diff) > threshold) {
|
if (Math.abs(diff) > threshold) return `am ${formatDate(date, time)}`;
|
||||||
const datestr = formatDate(date, time);
|
|
||||||
return cap ? datestr : "am" + datestr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const intl = new Intl.RelativeTimeFormat(LOCALE);
|
const intl = new Intl.RelativeTimeFormat(LOCALE);
|
||||||
const outstr = cap ? (s: string) => {
|
|
||||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
||||||
} : (s: string) => s;
|
|
||||||
|
|
||||||
const diffDays = dateDiffInDays(now, dt);
|
const diffDays = dateDiffInDays(now, dt);
|
||||||
if (diffDays !== 0) {
|
if (diffDays !== 0) {
|
||||||
if (diffDays === 1) return outstr("morgen");
|
if (diffDays === 1) return "morgen";
|
||||||
if (diffDays === -1) return outstr("gestern");
|
if (diffDays === -1) return "gestern";
|
||||||
return intl.format(diffDays, "day");
|
return intl.format(diffDays, "day");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +85,7 @@ export function humanDate(date: Date | string, time = false, cap = false): strin
|
||||||
if (diffMinutes !== 0) return intl.format(diffMinutes, "minute");
|
if (diffMinutes !== 0) return intl.format(diffMinutes, "minute");
|
||||||
}
|
}
|
||||||
|
|
||||||
return outstr(time ? "jetzt gerade" : "heute");
|
return time ? "jetzt gerade" : "heute";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DateRange {
|
export class DateRange {
|
||||||
|
@ -128,19 +106,14 @@ export class DateRange {
|
||||||
return new DateRange(start, end);
|
return new DateRange(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get a date range of the week containing the given date */
|
|
||||||
static weekOf(day: Date, offset = 0): DateRange {
|
|
||||||
const thisDay = new Date(day);
|
|
||||||
const todayWd = thisDay.getDay();
|
|
||||||
// Day starts at Sunday (0)
|
|
||||||
const daysMinus = (todayWd === 0 ? 6 : todayWd - 1) - offset * 7;
|
|
||||||
thisDay.setDate(thisDay.getDate() - daysMinus);
|
|
||||||
return DateRange.withLength(thisDay, 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a date range of the current calendar week */
|
/** Create a date range of the current calendar week */
|
||||||
static thisWeek(): DateRange {
|
static thisWeek(): DateRange {
|
||||||
return DateRange.weekOf(new Date());
|
const dayStart = new Date();
|
||||||
|
const todayWd = dayStart.getDay();
|
||||||
|
// Day starts at Sunday (0)
|
||||||
|
const daysMinus = todayWd === 0 ? 6 : todayWd - 1;
|
||||||
|
dayStart.setDate(dayStart.getDate() - daysMinus);
|
||||||
|
return DateRange.withLength(dayStart, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parse a date range from a string
|
/** Parse a date range from a string
|
||||||
|
@ -151,23 +124,10 @@ export class DateRange {
|
||||||
* - Range with 1 end: `2024-04-13..`; `..2024-04-20`
|
* - Range with 1 end: `2024-04-13..`; `..2024-04-20`
|
||||||
*/
|
*/
|
||||||
static parse(s: string, utc = false): DateRange | null {
|
static parse(s: string, utc = false): DateRange | null {
|
||||||
return DateRange.parseInternal(s, "..", (p) => utc ? new Date(p) : dateFromYMD(p));
|
const parts = s.split("..", 2);
|
||||||
}
|
|
||||||
|
|
||||||
/** Parse a date range from human input */
|
|
||||||
static parseHuman(s: string): DateRange | null {
|
|
||||||
return DateRange.parseInternal(s, "-", dateFromHuman);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static parseInternal(
|
|
||||||
s: string,
|
|
||||||
split: string | RegExp,
|
|
||||||
dateParser: (s: string) => Date,
|
|
||||||
): DateRange | null {
|
|
||||||
const parts = s.split(split, 2);
|
|
||||||
const parsed = parts.map((p) => {
|
const parsed = parts.map((p) => {
|
||||||
if (p.length === 0) return null;
|
if (p.length === 0) return null;
|
||||||
return dateParser(p);
|
return utc ? new Date(p) : dateFromYMD(p);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parsed.length === 0
|
if (parsed.length === 0
|
||||||
|
@ -185,8 +145,8 @@ export class DateRange {
|
||||||
|
|
||||||
/** Shift the range by the given number of days. This modifies the range in-place */
|
/** Shift the range by the given number of days. This modifies the range in-place */
|
||||||
addDays(n: number): void {
|
addDays(n: number): void {
|
||||||
this.start?.setUTCDate(this.start.getUTCDate() + n);
|
this.start?.setDate(this.start.getDate() + n);
|
||||||
this.end?.setUTCDate(this.end.getUTCDate() + n);
|
this.end?.setDate(this.end.getDate() + n);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return a parsable string representation */
|
/** Return a parsable string representation */
|
||||||
|
@ -202,54 +162,6 @@ export class DateRange {
|
||||||
format(): string {
|
format(): string {
|
||||||
if (this.start === null) return "bis " + formatDate(this.end!);
|
if (this.start === null) return "bis " + formatDate(this.end!);
|
||||||
if (this.end === null) return "ab " + formatDate(this.start);
|
if (this.end === null) return "ab " + formatDate(this.start);
|
||||||
if (this.start.getFullYear() === this.end.getFullYear()
|
|
||||||
&& this.start.getMonth() === this.end.getMonth()
|
|
||||||
&& this.start.getDate() === this.end.getDate()) return formatDate(this.start);
|
|
||||||
return formatDate(this.start) + " \u2013 " + formatDate(this.end);
|
return formatDate(this.start) + " \u2013 " + formatDate(this.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
eq(b: DateRange): boolean {
|
|
||||||
return dateEq(this.start, b.start) && dateEq(this.end, b.end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dateEq(d1: Date | null, d2: Date | null): boolean {
|
|
||||||
if (!d1 || !d2) return d1 === d2;
|
|
||||||
return d1.getUTCFullYear() === d2.getUTCFullYear()
|
|
||||||
&& d1.getUTCMonth() === d2.getUTCMonth()
|
|
||||||
&& d1.getUTCDate() === d2.getUTCDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shiftDateRange(dateRange: DateRange, week: boolean, fwd: boolean) {
|
|
||||||
let modDir = null;
|
|
||||||
if (dateRange.start === null) {
|
|
||||||
dateRange.start = new Date(dateRange.end!);
|
|
||||||
modDir = true;
|
|
||||||
}
|
|
||||||
if (dateRange.end === null) {
|
|
||||||
dateRange.end = new Date(dateRange.start!);
|
|
||||||
modDir = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inc = fwd ? 1 : -1;
|
|
||||||
const leader = fwd ? dateRange.end : dateRange.start;
|
|
||||||
|
|
||||||
if (week) {
|
|
||||||
// Align range with week
|
|
||||||
const lweek = DateRange.weekOf(leader);
|
|
||||||
if (dateRange.eq(lweek)) {
|
|
||||||
if (modDir === null || modDir === fwd) dateRange.addDays(inc * 7);
|
|
||||||
} else {
|
|
||||||
dateRange.start = lweek.start;
|
|
||||||
dateRange.end = lweek.end;
|
|
||||||
if (modDir === fwd) dateRange.addDays(inc * 7);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (dateEq(dateRange.start, dateRange.end)) {
|
|
||||||
dateRange.addDays(inc);
|
|
||||||
} else {
|
|
||||||
dateRange.start = leader;
|
|
||||||
dateRange.end = leader;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ it("getQueryUrl", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryUrl = getQueryUrl(query, "");
|
const queryUrl = getQueryUrl(query, "");
|
||||||
expect(queryUrl).toBe("?filter%5Bauthor%5D%5B0%5D%5Bid%5D=2&filter%5Bauthor%5D%5B0%5D%5Bname%5D=Max&filter%5Bcategory%5D%5B0%5D%5Bid%5D=1&filter%5Bcategory%5D%5B0%5D%5Bname%5D=Blutabnahme&filter%5Bcategory%5D%5B1%5D%5Bid%5D=2&filter%5Bcategory%5D%5B1%5D%5Bname%5D=Labortests&filter%5Bdone%5D=true&filter%5Bsearch%5D=Hello%20World&pagination%5Blimit%5D=10&pagination%5Boffset%5D=20&sort%5B0%5D=room");
|
expect(queryUrl).toBe("?filter%5Bauthor%5D%5B0%5D%5Bid%5D=2&filter%5Bauthor%5D%5B0%5D%5Bname%5D=Max&filter%5Bcategory%5D%5B0%5D%5Bid%5D=1&filter%5Bcategory%5D%5B0%5D%5Bname%5D=Blutabnahme&filter%5Bcategory%5D%5B1%5D%5Bid%5D=2&filter%5Bcategory%5D%5B1%5D%5Bname%5D=Labortests&filter%5Bdone%5D=true&filter%5Bsearch%5D=Hello%20World&pagination%5Blimit%5D=10&pagination%5Boffset%5D=20&sort%5Bfield%5D=room&sort%5Basc%5D=true");
|
||||||
|
|
||||||
const decoded = ZEntriesQuery.parse(parseQueryUrl(queryUrl));
|
const decoded = ZEntriesQuery.parse(parseQueryUrl(queryUrl));
|
||||||
expect(decoded).toStrictEqual(query);
|
expect(decoded).toStrictEqual(query);
|
||||||
|
|
|
@ -161,7 +161,7 @@ export function defaultVisitUrl(): string {
|
||||||
export async function moveEntryTodoDate(id: number, nTodoDays: number, init?: TRPCClientInit) {
|
export async function moveEntryTodoDate(id: number, nTodoDays: number, init?: TRPCClientInit) {
|
||||||
if (nTodoDays > 0) {
|
if (nTodoDays > 0) {
|
||||||
const entry = await trpc(init).entry.get.query(id);
|
const entry = await trpc(init).entry.get.query(id);
|
||||||
const newDate = new Date();
|
const newDate = new Date(entry.current_version.date);
|
||||||
newDate.setDate(newDate.getDate() + nTodoDays);
|
newDate.setDate(newDate.getDate() + nTodoDays);
|
||||||
|
|
||||||
await trpc(init).entry.newVersion.mutate({
|
await trpc(init).entry.newVersion.mutate({
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import logo from "$lib/assets/icon.opt.svg";
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const version = __VERSION__;
|
const version = __VERSION__;
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
|
@ -8,7 +6,6 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<img alt="" src={logo} width="200" />
|
|
||||||
<h1 class="heading">Visitenbuch</h1>
|
<h1 class="heading">Visitenbuch</h1>
|
||||||
<p>Version: {version}</p>
|
<p>Version: {version}</p>
|
||||||
<p>Letzte Änderung: {lastmod}</p>
|
<p>Letzte Änderung: {lastmod}</p>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||||
import { trpc } from "$lib/shared/trpc";
|
import { trpc } from "$lib/shared/trpc";
|
||||||
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util";
|
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util";
|
||||||
|
|
||||||
import { SchemaNewExecution } from "./schema";
|
import { SchemaNewExecution } from "./editExecution/schema";
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async (event) => loadWrap(async () => {
|
default: async (event) => loadWrap(async () => {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
import EntryTodoButton from "$lib/components/ui/EntryTodoButton.svelte";
|
import EntryTodoButton from "$lib/components/ui/EntryTodoButton.svelte";
|
||||||
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
||||||
|
|
||||||
import { SchemaNewExecution } from "./schema";
|
import { SchemaNewExecution } from "./editExecution/schema";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||||
import { trpc } from "$lib/shared/trpc";
|
import { trpc } from "$lib/shared/trpc";
|
||||||
import { loadWrap } from "$lib/shared/util";
|
import { loadWrap } from "$lib/shared/util";
|
||||||
|
|
||||||
import { SchemaNewExecution } from "./schema";
|
import { SchemaNewExecution } from "./editExecution/schema";
|
||||||
|
|
||||||
export const load: PageLoad = async (event) => {
|
export const load: PageLoad = async (event) => {
|
||||||
const entry = await loadWrap(async () => {
|
const entry = await loadWrap(async () => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||||
import { trpc } from "$lib/shared/trpc";
|
import { trpc } from "$lib/shared/trpc";
|
||||||
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util";
|
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util";
|
||||||
|
|
||||||
import { SchemaNewExecution } from "../schema";
|
import { SchemaNewExecution } from "./schema";
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async (event) => loadWrap(async () => {
|
default: async (event) => loadWrap(async () => {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
import EntryTodoButton from "$lib/components/ui/EntryTodoButton.svelte";
|
import EntryTodoButton from "$lib/components/ui/EntryTodoButton.svelte";
|
||||||
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
||||||
|
|
||||||
import { SchemaNewExecution } from "../schema";
|
import { SchemaNewExecution } from "./schema";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||||
import { trpc } from "$lib/shared/trpc";
|
import { trpc } from "$lib/shared/trpc";
|
||||||
import { loadWrap } from "$lib/shared/util";
|
import { loadWrap } from "$lib/shared/util";
|
||||||
|
|
||||||
import { SchemaNewExecution } from "../schema";
|
import { SchemaNewExecution } from "./schema";
|
||||||
|
|
||||||
export const load: PageLoad = async (event) => {
|
export const load: PageLoad = async (event) => {
|
||||||
const entry = await loadWrap(async () => {
|
const entry = await loadWrap(async () => {
|
||||||
|
|
9
src/routes/(app)/entry/[id]/editExecution/schema.ts
Normal file
9
src/routes/(app)/entry/[id]/editExecution/schema.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { zod } from "sveltekit-superforms/adapters";
|
||||||
|
|
||||||
|
import { ZEntryExecutionNew, fields } from "$lib/shared/model/validation";
|
||||||
|
|
||||||
|
export const SchemaNewExecution = zod(
|
||||||
|
ZEntryExecutionNew.extend({
|
||||||
|
old_execution_id: fields.EntityId().optional(),
|
||||||
|
}),
|
||||||
|
);
|
|
@ -1,9 +1,10 @@
|
||||||
import { zod } from "sveltekit-superforms/adapters";
|
import { zod } from "sveltekit-superforms/adapters";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ZEntryExecutionNew, fields } from "$lib/shared/model/validation";
|
import { fields } from "$lib/shared/model/validation";
|
||||||
|
|
||||||
export const SchemaNewExecution = zod(
|
const ZEntryDone = z.object({
|
||||||
ZEntryExecutionNew.extend({
|
text: fields.TextString(),
|
||||||
old_execution_id: fields.EntityId().optional(),
|
});
|
||||||
}),
|
|
||||||
);
|
export const SchemaEntryExecution = zod(ZEntryDone);
|
||||||
|
|
|
@ -3,18 +3,13 @@
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
import { mdiCalendar, mdiDomain } from "@mdi/js";
|
|
||||||
|
|
||||||
import { URL_VISIT } from "$lib/shared/constants";
|
import { URL_VISIT } from "$lib/shared/constants";
|
||||||
import type { PaginationRequest, Station } from "$lib/shared/model";
|
import type { PaginationRequest, Station } from "$lib/shared/model";
|
||||||
import { trpc } from "$lib/shared/trpc";
|
import { trpc } from "$lib/shared/trpc";
|
||||||
import {
|
import { DateRange, getQueryUrl } from "$lib/shared/util";
|
||||||
DateRange, dateFromYMD, getQueryUrl, humanDate,
|
|
||||||
} from "$lib/shared/util";
|
|
||||||
|
|
||||||
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
||||||
import EntryCard from "$lib/components/ui/EntryCard.svelte";
|
import EntryCard from "$lib/components/ui/EntryCard.svelte";
|
||||||
import IconButton from "$lib/components/ui/IconButton.svelte";
|
|
||||||
import PaginationButtons from "$lib/components/ui/PaginationButtons.svelte";
|
import PaginationButtons from "$lib/components/ui/PaginationButtons.svelte";
|
||||||
import WeekSelector from "$lib/components/ui/WeekSelector.svelte";
|
import WeekSelector from "$lib/components/ui/WeekSelector.svelte";
|
||||||
|
|
||||||
|
@ -45,12 +40,9 @@
|
||||||
selection = null;
|
selection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: groupByStation = data.query.group === "station";
|
|
||||||
|
|
||||||
function paginationUpdate(pagination: PaginationRequest): void {
|
function paginationUpdate(pagination: PaginationRequest): void {
|
||||||
updateQuery({
|
updateQuery({
|
||||||
filter: data.query.filter,
|
filter: data.query.filter,
|
||||||
group: data.query.group,
|
|
||||||
pagination,
|
pagination,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -62,14 +54,6 @@
|
||||||
station: selection ? [selection] : undefined,
|
station: selection ? [selection] : undefined,
|
||||||
date: dateRange ? [{ id: dateRange.toString() }] : undefined,
|
date: dateRange ? [{ id: dateRange.toString() }] : undefined,
|
||||||
},
|
},
|
||||||
group: data.query.group,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleGroup(): void {
|
|
||||||
updateQuery({
|
|
||||||
...data.query,
|
|
||||||
group: groupByStation ? undefined : "station",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,16 +74,11 @@
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 justify-between">
|
<div class="flex flex-wrap gap-2 justify-between">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<!-- <span>Zeitraum:</span> -->
|
<span>Woche:</span>
|
||||||
<WeekSelector onSelect={filterUpdate} bind:dateRange />
|
<WeekSelector onSelect={filterUpdate} bind:dateRange />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex gap-2 items-center">
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>Gruppierung:</span>
|
|
||||||
<IconButton ariaLabel={groupByStation ? "Station" : "Datum"} path={groupByStation ? mdiDomain : mdiCalendar} on:click={toggleGroup} />
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>Station:</span>
|
<span>Station:</span>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
clearBtn
|
clearBtn
|
||||||
|
@ -110,33 +89,15 @@
|
||||||
bind:selection />
|
bind:selection />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
{#each data.groups as group}
|
{#each data.entries.items as entry}
|
||||||
{@const first = group.items[0] ?? group.prio[0]}
|
|
||||||
<div class="bg-base-content/15 rounded-xl px-2 font-bold">
|
|
||||||
{#if data.groupByStation}
|
|
||||||
{#if first.patient.room}
|
|
||||||
Station {first.patient.room?.station.name}
|
|
||||||
{:else}
|
|
||||||
Keine Station
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
{humanDate(dateFromYMD(first.current_version.date), false, true)}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#each group.prio as entry}
|
|
||||||
<EntryCard {entry} />
|
<EntryCard {entry} />
|
||||||
{/each}
|
{/each}
|
||||||
{#each group.items as entry}
|
|
||||||
<EntryCard {entry} />
|
|
||||||
{/each}
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PaginationButtons
|
<PaginationButtons
|
||||||
data={data.pagination}
|
data={data.entries}
|
||||||
onUpdate={paginationUpdate}
|
onUpdate={paginationUpdate}
|
||||||
paginationData={data.query.pagination}
|
paginationData={data.query.pagination}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,16 +4,9 @@ import { redirect } from "@sveltejs/kit";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||||
import { trpc, type RouterOutput } from "$lib/shared/trpc";
|
import { trpc } from "$lib/shared/trpc";
|
||||||
import { defaultVisitUrl, loadWrap, parseQueryUrl } from "$lib/shared/util";
|
import { defaultVisitUrl, loadWrap, parseQueryUrl } from "$lib/shared/util";
|
||||||
|
|
||||||
type EntryItems = RouterOutput["entry"]["list"]["items"];
|
|
||||||
type EntryItem = EntryItems[0];
|
|
||||||
type EntryGroup = {
|
|
||||||
items: EntryItems,
|
|
||||||
prio: EntryItems,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const load: PageLoad = async (event) => {
|
export const load: PageLoad = async (event) => {
|
||||||
return loadWrap(async () => {
|
return loadWrap(async () => {
|
||||||
let query: z.infer<typeof ZEntriesQuery> = {};
|
let query: z.infer<typeof ZEntriesQuery> = {};
|
||||||
|
@ -27,49 +20,20 @@ export const load: PageLoad = async (event) => {
|
||||||
redirect(302, defaultVisitUrl());
|
redirect(302, defaultVisitUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupByStation = query.group === "station";
|
|
||||||
|
|
||||||
// Sort entries by date
|
// Sort entries by date
|
||||||
query.sort = ["date", "room"];
|
if (!query.sort) {
|
||||||
if (groupByStation) query.sort.reverse();
|
query.sort = ["priority:dsc", "date"];
|
||||||
|
}
|
||||||
|
|
||||||
const entries = await trpc(event).entry.list.query(query);
|
const entries = await trpc(event).entry.list.query(query);
|
||||||
|
|
||||||
// Group items by date
|
// Move prioritized items to the front
|
||||||
const getGroupKey = groupByStation
|
entries.items.forEach((itm, i) => {
|
||||||
? (e: EntryItem) => e.patient.room?.station.id
|
if (itm.current_version.priority) {
|
||||||
: (e: EntryItem) => e.current_version.date;
|
entries.items.unshift(entries.items.splice(i, 1)[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const groups: EntryGroup[] = [];
|
return { query, entries };
|
||||||
let dg = null;
|
|
||||||
let items: EntryItems = [];
|
|
||||||
let prio: EntryItems = [];
|
|
||||||
|
|
||||||
for (const entry of entries.items) {
|
|
||||||
// New group
|
|
||||||
if (getGroupKey(entry) !== dg) {
|
|
||||||
dg = getGroupKey(entry);
|
|
||||||
if (items.length > 0 || prio.length > 0) {
|
|
||||||
groups.push({ items, prio });
|
|
||||||
items = [];
|
|
||||||
prio = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (entry.current_version.priority) {
|
|
||||||
prio.push(entry);
|
|
||||||
} else {
|
|
||||||
items.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (items.length > 0 || prio.length > 0) {
|
|
||||||
groups.push({ items, prio });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
query,
|
|
||||||
groups,
|
|
||||||
pagination: { offset: entries.offset, total: entries.total, nItems: entries.items.length },
|
|
||||||
groupByStation,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.5 KiB |
Loading…
Reference in a new issue