From 157cbc425e9edd9197ac43924d67783d77fac802 Mon Sep 17 00:00:00 2001 From: Piotr Oleszczyk Date: Tue, 10 Mar 2026 12:44:19 +0100 Subject: [PATCH] feat(frontend): improve lab result filter ergonomics --- backend/innercontext/api/health.py | 3 + frontend/messages/en.json | 10 + frontend/messages/pl.json | 10 + frontend/src/app.css | 17 ++ frontend/src/lib/api.ts | 6 + .../routes/health/lab-results/+page.server.ts | 27 ++- .../routes/health/lab-results/+page.svelte | 196 ++++++++++++++---- 7 files changed, 221 insertions(+), 48 deletions(-) diff --git a/backend/innercontext/api/health.py b/backend/innercontext/api/health.py index 86e67af..d3e8064 100644 --- a/backend/innercontext/api/health.py +++ b/backend/innercontext/api/health.py @@ -265,6 +265,7 @@ def list_lab_results( test_code: Optional[str] = None, flag: Optional[ResultFlag] = None, flags: list[ResultFlag] = Query(default_factory=list), + without_flag: bool = False, from_date: Optional[datetime] = None, to_date: Optional[datetime] = None, latest_only: bool = False, @@ -287,6 +288,8 @@ def list_lab_results( filters.append(LabResult.flag == flag) if flags: filters.append(col(LabResult.flag).in_(flags)) + if without_flag: + filters.append(col(LabResult.flag).is_(None)) if from_date is not None: filters.append(LabResult.collected_at >= from_date) if to_date is not None: diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 9dcc24a..651301a 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -367,6 +367,16 @@ "labResults_flagFilter": "Flag:", "labResults_flagAll": "All", "labResults_flagNone": "None", + "labResults_statusAll": "All", + "labResults_statusAbnormal": "Abnormal", + "labResults_statusNormal": "Normal", + "labResults_statusUninterpreted": "No interpretation", + "labResults_activeFilters": "Active filters", + "labResults_activeFilterSearch": "Search: {value}", + "labResults_activeFilterCode": "Code: {value}", + "labResults_activeFilterFrom": "From: {value}", + "labResults_activeFilterTo": "To: {value}", + "labResults_activeFilterHistory": "Full history", "labResults_date": "Date *", "labResults_loincCode": "LOINC code *", "labResults_loincExample": "e.g. 718-7", diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index b0fca21..33fbc31 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -387,6 +387,16 @@ "labResults_flagFilter": "Flaga:", "labResults_flagAll": "Wszystkie", "labResults_flagNone": "Brak", + "labResults_statusAll": "Wszystkie", + "labResults_statusAbnormal": "Nieprawidłowe", + "labResults_statusNormal": "Prawidłowe", + "labResults_statusUninterpreted": "Bez interpretacji", + "labResults_activeFilters": "Aktywne filtry", + "labResults_activeFilterSearch": "Szukaj: {value}", + "labResults_activeFilterCode": "Kod: {value}", + "labResults_activeFilterFrom": "Od: {value}", + "labResults_activeFilterTo": "Do: {value}", + "labResults_activeFilterHistory": "Pełna historia", "labResults_date": "Data *", "labResults_loincCode": "Kod LOINC *", "labResults_loincExample": "np. 718-7", diff --git a/frontend/src/app.css b/frontend/src/app.css index 4aa2272..9388644 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -413,6 +413,23 @@ body { margin-bottom: 0.65rem; } +.lab-results-filter-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + border: 1px solid color-mix(in srgb, var(--page-accent) 30%, var(--border)); + border-radius: 999px; + background: color-mix(in srgb, var(--page-accent) 10%, white); + padding: 0.35rem 0.65rem; + color: var(--foreground); + font-size: 0.78rem; + text-decoration: none; +} + +.lab-results-filter-chip:hover { + background: color-mix(in srgb, var(--page-accent) 16%, white); +} + .editorial-alert { border-radius: 0.7rem; border: 1px solid hsl(34 25% 75% / 0.8); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index f03a41e..9e45b4e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -274,6 +274,8 @@ export interface LabResultListParams { q?: string; test_code?: string; flag?: string; + flags?: string[]; + without_flag?: boolean; from_date?: string; to_date?: string; latest_only?: boolean; @@ -295,6 +297,10 @@ export function getLabResults( if (params.q) q.set("q", params.q); if (params.test_code) q.set("test_code", params.test_code); if (params.flag) q.set("flag", params.flag); + if (params.flags?.length) { + for (const flag of params.flags) q.append("flags", flag); + } + if (params.without_flag != null) q.set("without_flag", String(params.without_flag)); if (params.from_date) q.set("from_date", params.from_date); if (params.to_date) q.set("to_date", params.to_date); if (params.latest_only != null) q.set("latest_only", String(params.latest_only)); diff --git a/frontend/src/routes/health/lab-results/+page.server.ts b/frontend/src/routes/health/lab-results/+page.server.ts index 13cd386..1c9fa4f 100644 --- a/frontend/src/routes/health/lab-results/+page.server.ts +++ b/frontend/src/routes/health/lab-results/+page.server.ts @@ -2,10 +2,18 @@ import { deleteLabResult, getLabResults, updateLabResult } from '$lib/api'; import { fail } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; +const STATUS_GROUP_FLAGS = { + abnormal: ['ABN', 'H', 'L', 'POS'], + normal: ['N', 'NEG'] +} as const; + +type StatusGroup = 'all' | 'abnormal' | 'normal' | 'uninterpreted'; + export const load: PageServerLoad = async ({ url }) => { const q = url.searchParams.get('q') ?? undefined; const test_code = url.searchParams.get('test_code') ?? undefined; const flag = url.searchParams.get('flag') ?? undefined; + const status_group = normalizeStatusGroup(url.searchParams.get('status_group')); const from_date = url.searchParams.get('from_date') ?? undefined; const to_date = url.searchParams.get('to_date') ?? undefined; const requestedLatestOnly = url.searchParams.get('latest_only') !== 'false'; @@ -15,10 +23,19 @@ export const load: PageServerLoad = async ({ url }) => { const limit = 50; const offset = (page - 1) * limit; + const statusFlags = status_group === 'all' || status_group === 'uninterpreted' + ? [] + : [...STATUS_GROUP_FLAGS[status_group]]; + const effectiveFlag = flag && statusFlags.includes(flag as (typeof statusFlags)[number]) ? flag : undefined; + const flags = effectiveFlag ? undefined : statusFlags; + const without_flag = status_group === 'uninterpreted' ? true : undefined; + const resultPage = await getLabResults({ q, test_code, - flag, + flag: effectiveFlag, + flags, + without_flag, from_date, to_date, latest_only: latestOnly, @@ -31,7 +48,8 @@ export const load: PageServerLoad = async ({ url }) => { resultPage, q, test_code, - flag, + flag: effectiveFlag, + status_group, from_date, to_date, latestOnly, @@ -40,6 +58,11 @@ export const load: PageServerLoad = async ({ url }) => { }; }; +function normalizeStatusGroup(value: string | null): StatusGroup { + if (value === 'abnormal' || value === 'normal' || value === 'uninterpreted') return value; + return 'all'; +} + export const actions: Actions = { update: async ({ request }) => { const form = await request.formData(); diff --git a/frontend/src/routes/health/lab-results/+page.svelte b/frontend/src/routes/health/lab-results/+page.svelte index 84f205d..fe2a92c 100644 --- a/frontend/src/routes/health/lab-results/+page.svelte +++ b/frontend/src/routes/health/lab-results/+page.svelte @@ -1,4 +1,5 @@