205 lines
7.2 KiB
Svelte
205 lines
7.2 KiB
Svelte
<script lang="ts">
|
|
import { enhance } from '$app/forms';
|
|
import { goto } from '$app/navigation';
|
|
import { resolve } from '$app/paths';
|
|
import type { ActionData, PageData } from './$types';
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
import { Button } from '$lib/components/ui/button';
|
|
import { Input } from '$lib/components/ui/input';
|
|
import { Label } from '$lib/components/ui/label';
|
|
import { baseSelectClass } from '$lib/components/forms/form-classes';
|
|
import FormSectionCard from '$lib/components/forms/FormSectionCard.svelte';
|
|
import SimpleSelect from '$lib/components/forms/SimpleSelect.svelte';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow
|
|
} from '$lib/components/ui/table';
|
|
|
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
|
|
|
const flags = ['N', 'ABN', 'POS', 'NEG', 'L', 'H'];
|
|
const flagPills: Record<string, string> = {
|
|
N: 'health-flag-pill health-flag-pill--normal',
|
|
ABN: 'health-flag-pill health-flag-pill--abnormal',
|
|
POS: 'health-flag-pill health-flag-pill--positive',
|
|
NEG: 'health-flag-pill health-flag-pill--negative',
|
|
L: 'health-flag-pill health-flag-pill--low',
|
|
H: 'health-flag-pill health-flag-pill--high'
|
|
};
|
|
|
|
let showForm = $state(false);
|
|
let selectedFlag = $state('');
|
|
let filterFlag = $derived(data.flag ?? '');
|
|
|
|
const flagOptions = flags.map((f) => ({ value: f, label: f }));
|
|
|
|
function onFlagChange(v: string) {
|
|
const base = resolve('/health/lab-results');
|
|
const target = v ? `${base}?flag=${encodeURIComponent(v)}` : base;
|
|
goto(target, { replaceState: true });
|
|
}
|
|
</script>
|
|
|
|
<svelte:head><title>{m["labResults_title"]()} — innercontext</title></svelte:head>
|
|
|
|
<div class="editorial-page space-y-4">
|
|
<section class="editorial-hero reveal-1">
|
|
<p class="editorial-kicker">{m["nav_appSubtitle"]()}</p>
|
|
<h2 class="editorial-title">{m["labResults_title"]()}</h2>
|
|
<p class="editorial-subtitle">{m["labResults_count"]({ count: data.results.length })}</p>
|
|
<div class="editorial-toolbar">
|
|
<Button href={resolve('/health/medications')} variant="outline">{m.medications_title()}</Button>
|
|
<Button variant="outline" onclick={() => (showForm = !showForm)}>
|
|
{showForm ? m.common_cancel() : m["labResults_addNew"]()}
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
|
|
{#if form?.error}
|
|
<div class="editorial-alert editorial-alert--error">{form.error}</div>
|
|
{/if}
|
|
{#if form?.created}
|
|
<div class="editorial-alert editorial-alert--success">{m["labResults_added"]()}</div>
|
|
{/if}
|
|
|
|
<!-- Filter -->
|
|
<div class="editorial-panel reveal-2 flex items-center gap-3">
|
|
<span class="text-sm text-muted-foreground">{m["labResults_flagFilter"]()}</span>
|
|
<select
|
|
class={`${baseSelectClass} w-32`}
|
|
value={filterFlag}
|
|
onchange={(e) => onFlagChange(e.currentTarget.value)}
|
|
>
|
|
<option value="">{m["labResults_flagAll"]()}</option>
|
|
{#each flags as f (f)}
|
|
<option value={f}>{f}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
|
|
{#if showForm}
|
|
<FormSectionCard title={m["labResults_newTitle"]()} className="reveal-2">
|
|
<form method="POST" action="?/create" use:enhance class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<Label for="collected_at">{m["labResults_date"]()}</Label>
|
|
<Input id="collected_at" name="collected_at" type="date" required />
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Label for="test_code">{m["labResults_loincCode"]()} <span class="text-xs text-muted-foreground">({m["labResults_loincExample"]()})</span></Label>
|
|
<Input id="test_code" name="test_code" required placeholder="718-7" />
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Label for="test_name_original">{m["labResults_testName"]()}</Label>
|
|
<Input id="test_name_original" name="test_name_original" placeholder={m["labResults_testNamePlaceholder"]()} />
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Label for="lab">{m["labResults_lab"]()}</Label>
|
|
<Input id="lab" name="lab" placeholder={m["labResults_labPlaceholder"]()} />
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Label for="value_num">{m["labResults_value"]()}</Label>
|
|
<Input id="value_num" name="value_num" type="number" step="any" />
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Label for="unit_original">{m["labResults_unit"]()}</Label>
|
|
<Input id="unit_original" name="unit_original" placeholder={m["labResults_unitPlaceholder"]()} />
|
|
</div>
|
|
<SimpleSelect
|
|
id="flag"
|
|
name="flag"
|
|
label={m["labResults_flag"]()}
|
|
options={flagOptions}
|
|
placeholder={m["labResults_flagNone"]()}
|
|
bind:value={selectedFlag}
|
|
/>
|
|
<div class="flex items-end">
|
|
<Button type="submit">{m.common_add()}</Button>
|
|
</div>
|
|
</form>
|
|
</FormSectionCard>
|
|
{/if}
|
|
|
|
<!-- Desktop: table -->
|
|
<div class="products-table-shell hidden md:block reveal-2">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{m["labResults_colDate"]()}</TableHead>
|
|
<TableHead>{m["labResults_colTest"]()}</TableHead>
|
|
<TableHead>{m["labResults_colLoinc"]()}</TableHead>
|
|
<TableHead>{m["labResults_colValue"]()}</TableHead>
|
|
<TableHead>{m["labResults_colFlag"]()}</TableHead>
|
|
<TableHead>{m["labResults_colLab"]()}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{#each data.results as r (r.record_id)}
|
|
<TableRow>
|
|
<TableCell class="text-sm">{r.collected_at.slice(0, 10)}</TableCell>
|
|
<TableCell class="font-medium">{r.test_name_original ?? r.test_code}</TableCell>
|
|
<TableCell class="text-xs text-muted-foreground font-mono">{r.test_code}</TableCell>
|
|
<TableCell>
|
|
{#if r.value_num != null}
|
|
{r.value_num} {r.unit_original ?? ''}
|
|
{:else if r.value_text}
|
|
{r.value_text}
|
|
{:else}
|
|
—
|
|
{/if}
|
|
</TableCell>
|
|
<TableCell>
|
|
{#if r.flag}
|
|
<span class={flagPills[r.flag] ?? 'health-flag-pill'}>
|
|
{r.flag}
|
|
</span>
|
|
{:else}
|
|
—
|
|
{/if}
|
|
</TableCell>
|
|
<TableCell class="text-sm text-muted-foreground">{r.lab ?? '—'}</TableCell>
|
|
</TableRow>
|
|
{:else}
|
|
<TableRow>
|
|
<TableCell colspan={6} class="text-center text-muted-foreground py-8">
|
|
{m["labResults_noResults"]()}
|
|
</TableCell>
|
|
</TableRow>
|
|
{/each}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<!-- Mobile: cards -->
|
|
<div class="flex flex-col gap-3 md:hidden reveal-3">
|
|
{#each data.results as r (r.record_id)}
|
|
<div class="products-mobile-card flex flex-col gap-1">
|
|
<div class="flex items-start justify-between gap-2">
|
|
<span class="font-medium">{r.test_name_original ?? r.test_code}</span>
|
|
{#if r.flag}
|
|
<span class={flagPills[r.flag] ?? 'health-flag-pill'}>
|
|
{r.flag}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
<p class="text-sm text-muted-foreground">{r.collected_at.slice(0, 10)}</p>
|
|
<div class="flex items-center gap-2 text-sm">
|
|
<span class="font-mono text-xs text-muted-foreground">{r.test_code}</span>
|
|
{#if r.value_num != null}
|
|
<span>{r.value_num} {r.unit_original ?? ''}</span>
|
|
{:else if r.value_text}
|
|
<span>{r.value_text}</span>
|
|
{/if}
|
|
</div>
|
|
{#if r.lab}
|
|
<p class="text-xs text-muted-foreground">{r.lab}</p>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<p class="py-8 text-center text-sm text-muted-foreground">{m["labResults_noResults"]()}</p>
|
|
{/each}
|
|
</div>
|
|
</div>
|