feat: add API client, types, layout, and all page routes
This commit is contained in:
parent
2e4e3fba50
commit
b140c55cda
71 changed files with 3237 additions and 58 deletions
44
frontend/src/routes/health/lab-results/+page.server.ts
Normal file
44
frontend/src/routes/health/lab-results/+page.server.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { createLabResult, getLabResults } from '$lib/api';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
const flag = url.searchParams.get('flag') ?? undefined;
|
||||
const from_date = url.searchParams.get('from_date') ?? undefined;
|
||||
const results = await getLabResults({ flag, from_date });
|
||||
return { results, flag };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
create: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
const collected_at = form.get('collected_at') as string;
|
||||
const test_code = form.get('test_code') as string;
|
||||
const test_name_original = form.get('test_name_original') as string;
|
||||
const value_num = form.get('value_num') as string;
|
||||
const unit_original = form.get('unit_original') as string;
|
||||
const flag = form.get('flag') as string;
|
||||
const lab = form.get('lab') as string;
|
||||
|
||||
if (!collected_at || !test_code) {
|
||||
return fail(400, { error: 'Date and test code are required' });
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
collected_at,
|
||||
test_code
|
||||
};
|
||||
if (test_name_original) body.test_name_original = test_name_original;
|
||||
if (value_num) body.value_num = Number(value_num);
|
||||
if (unit_original) body.unit_original = unit_original;
|
||||
if (flag) body.flag = flag;
|
||||
if (lab) body.lab = lab;
|
||||
|
||||
try {
|
||||
await createLabResult(body);
|
||||
return { created: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
}
|
||||
};
|
||||
176
frontend/src/routes/health/lab-results/+page.svelte
Normal file
176
frontend/src/routes/health/lab-results/+page.svelte
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/components/ui/table';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||
|
||||
const flags = ['N', 'ABN', 'POS', 'NEG', 'L', 'H'];
|
||||
const flagColors: Record<string, string> = {
|
||||
N: 'bg-green-100 text-green-800',
|
||||
ABN: 'bg-red-100 text-red-800',
|
||||
POS: 'bg-orange-100 text-orange-800',
|
||||
NEG: 'bg-blue-100 text-blue-800',
|
||||
L: 'bg-yellow-100 text-yellow-800',
|
||||
H: 'bg-red-100 text-red-800'
|
||||
};
|
||||
|
||||
let showForm = $state(false);
|
||||
let selectedFlag = $state('');
|
||||
let filterFlag = $derived(data.flag ?? '');
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Lab Results — innercontext</title></svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Lab Results</h2>
|
||||
<p class="text-muted-foreground">{data.results.length} results</p>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => (showForm = !showForm)}>
|
||||
{showForm ? 'Cancel' : '+ Add result'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">{form.error}</div>
|
||||
{/if}
|
||||
{#if form?.created}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Result added.</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-muted-foreground">Flag:</span>
|
||||
<Select
|
||||
type="single"
|
||||
value={filterFlag}
|
||||
onValueChange={(v) => {
|
||||
filterFlag = v;
|
||||
goto(v ? `/health/lab-results?flag=${v}` : '/health/lab-results');
|
||||
}}
|
||||
>
|
||||
<SelectTrigger class="w-32">{filterFlag || 'All'}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">All</SelectItem>
|
||||
{#each flags as f}
|
||||
<SelectItem value={f}>{f}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{#if showForm}
|
||||
<Card>
|
||||
<CardHeader><CardTitle>New lab result</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form method="POST" action="?/create" use:enhance class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<Label for="collected_at">Date *</Label>
|
||||
<Input id="collected_at" name="collected_at" type="date" required />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="test_code">LOINC code * <span class="text-xs text-muted-foreground">(e.g. 718-7)</span></Label>
|
||||
<Input id="test_code" name="test_code" required placeholder="718-7" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="test_name_original">Test name</Label>
|
||||
<Input id="test_name_original" name="test_name_original" placeholder="e.g. Hemoglobin" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="lab">Lab</Label>
|
||||
<Input id="lab" name="lab" placeholder="e.g. LabCorp" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="value_num">Value</Label>
|
||||
<Input id="value_num" name="value_num" type="number" step="any" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="unit_original">Unit</Label>
|
||||
<Input id="unit_original" name="unit_original" placeholder="e.g. g/dL" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Flag</Label>
|
||||
<input type="hidden" name="flag" value={selectedFlag} />
|
||||
<Select type="single" value={selectedFlag} onValueChange={(v) => (selectedFlag = v)}>
|
||||
<SelectTrigger>{selectedFlag || 'None'}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">None</SelectItem>
|
||||
{#each flags as f}
|
||||
<SelectItem value={f}>{f}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button type="submit">Add</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border border-border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Test</TableHead>
|
||||
<TableHead>LOINC</TableHead>
|
||||
<TableHead>Value</TableHead>
|
||||
<TableHead>Flag</TableHead>
|
||||
<TableHead>Lab</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.results as r}
|
||||
<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="rounded-full px-2 py-0.5 text-xs font-medium {flagColors[r.flag] ?? ''}">
|
||||
{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">
|
||||
No lab results found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue