mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
feat(modules): unify Vorsorge, Illness and Labs with the medication pattern
- add a wrench beside each module's add button linking to the module's own settings page; rename every add button to a plain "hinzufügen" - give each module a settings page: view (cards/list) + manual reorder; Labs also gets a sort-order choice (by last reading) and biomarker CRUD/reorder - collapse the Illness "abgeschlossen" list by default - rebuild the Vorsorge card on the medication card's building blocks: a green "Jetzt messen" action, the interval as a chip beside the name, and an in-interval progress signal; drop the kebab "anpassen" and stray actions - persist per-module view/order/sort client-side; add reverse-coverage-clean strings across all six locales
This commit is contained in:
1 parent
ae3091df9e
commit
267dcad13d
22 files changed
+1834
-273
No files matched your search
@@ -20,6 +20,7 @@ import { formatReferenceRange } from "@/lib/labs/reference-range";
|
||||
import { formatLabValue } from "@/lib/labs/format-value";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { applyOrder, useModuleListPrefs } from "@/lib/module-list-prefs";
|
||||
|
||||
import { LabTrendSparkline } from "./lab-trend-sparkline";
|
||||
import { ReferenceRangeBadge } from "./reference-range-badge";
|
||||
@@ -79,6 +80,7 @@ function groupReadings(results: LabResultDto[]): MarkerGroup[] {
|
||||
|
||||
export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
|
||||
const { t } = useTranslations();
|
||||
const { prefs } = useModuleListPrefs("labs");
|
||||
|
||||
const listKey = queryKeys.labResultsList({
|
||||
biomarkerId: undefined,
|
||||
@@ -96,10 +98,18 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
|
||||
apiGet<LabResultListResponse>("/api/labs?limit=500&sortDir=desc"),
|
||||
});
|
||||
|
||||
const groups = useMemo(
|
||||
() => groupReadings(data?.results ?? []),
|
||||
[data?.results],
|
||||
);
|
||||
const groups = useMemo(() => {
|
||||
const base = groupReadings(data?.results ?? []);
|
||||
// v1.18.6 (MOD-04) — honour the user's Labs sort choice. `groupReadings`
|
||||
// already returns most-recent-first; `recentAsc` reverses, and `manual`
|
||||
// applies the persisted biomarker order (legacy un-linked groups, which
|
||||
// carry no biomarkerId, sort after the ordered block).
|
||||
if (prefs.sortDir === "recentAsc") return [...base].reverse();
|
||||
if (prefs.sortDir === "manual") {
|
||||
return applyOrder(base, prefs.order, (g) => g.biomarkerId ?? g.key);
|
||||
}
|
||||
return base;
|
||||
}, [data?.results, prefs.sortDir, prefs.order]);
|
||||
|
||||
// The list caps at 500 rows server-side (the `limit` ceiling). Surface a
|
||||
// calm "showing latest N of M" hint when the cap truncates so the count is
|
||||
@@ -149,6 +159,65 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
// v1.18.6 (MOD-03) — compact list view: one bordered card holding tight
|
||||
// divided rows instead of a card per biomarker. The card view stays the
|
||||
// default; this is the denser alternative the settings toggle selects.
|
||||
if (prefs.view === "list") {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{truncated ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("labs.showingLatestOf", { shown, total })}
|
||||
</p>
|
||||
) : null}
|
||||
<Card>
|
||||
<CardContent className="divide-border divide-y p-0">
|
||||
{groups.map((group) => {
|
||||
const row = (
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-2.5">
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 text-sm">
|
||||
<span className="truncate font-medium">
|
||||
{group.analyte}
|
||||
</span>
|
||||
<ReferenceRangeBadge status={group.latest.rangeStatus} />
|
||||
</div>
|
||||
<div className="text-muted-foreground flex flex-wrap items-center gap-x-2 text-xs">
|
||||
<span className="text-foreground font-semibold tabular-nums">
|
||||
{formatLabValue(group.latest.value)}{" "}
|
||||
{group.latest.unit}
|
||||
</span>
|
||||
<span>{formatDate(group.latest.takenAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LabTrendSparkline
|
||||
values={group.readings.map((r) => r.value)}
|
||||
/>
|
||||
{group.biomarkerId ? (
|
||||
<ChevronRight className="text-muted-foreground h-4 w-4 shrink-0" />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return group.biomarkerId ? (
|
||||
<Link
|
||||
key={group.key}
|
||||
href={`/labs/${group.biomarkerId}`}
|
||||
className="hover:bg-muted/40 block transition-colors"
|
||||
>
|
||||
{row}
|
||||
</Link>
|
||||
) : (
|
||||
<div key={group.key}>{row}</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{truncated ? (
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.18.6 (W8 / MOD-03 + MOD-04) — the Labs module's own settings page body.
|
||||
*
|
||||
* Reached from the wrench beside the page's "hinzufügen" button. Lets the
|
||||
* user:
|
||||
* - pick the card-vs-list page view (MOD-03),
|
||||
* - choose the sort order (by most-recent reading, descending / ascending,
|
||||
* or manual) — MOD-04,
|
||||
* - reorder biomarkers when sort = manual (MOD-03/04),
|
||||
* - and create / edit / delete biomarkers via the existing
|
||||
* `BiomarkerManager` (MOD-04).
|
||||
* View / sort / order persist client-side via `useModuleListPrefs("labs")`.
|
||||
*/
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { NativeSelect } from "@/components/ui/native-select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { ModuleViewToggle } from "@/components/module-list/module-view-toggle";
|
||||
import {
|
||||
ModuleOrderEditor,
|
||||
type ReorderItem,
|
||||
} from "@/components/module-list/module-order-editor";
|
||||
import {
|
||||
applyOrder,
|
||||
useModuleListPrefs,
|
||||
type ModuleSortDir,
|
||||
} from "@/lib/module-list-prefs";
|
||||
|
||||
import { BiomarkerManager } from "./biomarker-manager";
|
||||
import type { BiomarkerDto, BiomarkerListResponse } from "./types";
|
||||
|
||||
export function LabsSettings() {
|
||||
const { t } = useTranslations();
|
||||
const { prefs, setView, setOrder, setSortDir } = useModuleListPrefs("labs");
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: queryKeys.biomarkers(),
|
||||
queryFn: () => apiGet<BiomarkerListResponse>("/api/biomarkers"),
|
||||
});
|
||||
const markers: BiomarkerDto[] = data?.biomarkers ?? [];
|
||||
|
||||
const ordered = applyOrder(markers, prefs.order, (m) => m.id);
|
||||
const reorderItems: ReorderItem[] = ordered.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
secondary: m.panel ? `${m.unit} · ${m.panel}` : m.unit,
|
||||
}));
|
||||
|
||||
const manual = prefs.sortDir === "manual";
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold">
|
||||
{t("moduleList.viewHeading")}
|
||||
</h2>
|
||||
<ModuleViewToggle view={prefs.view} onChange={setView} />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("moduleList.viewDescription")}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold">{t("labs.sort.heading")}</h2>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="labs-sort">{t("labs.sort.label")}</Label>
|
||||
<NativeSelect
|
||||
id="labs-sort"
|
||||
value={prefs.sortDir}
|
||||
onChange={(e) => setSortDir(e.target.value as ModuleSortDir)}
|
||||
>
|
||||
<option value="recentDesc">{t("labs.sort.recentDesc")}</option>
|
||||
<option value="recentAsc">{t("labs.sort.recentAsc")}</option>
|
||||
<option value="manual">{t("labs.sort.manual")}</option>
|
||||
</NativeSelect>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("labs.sort.hint")}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{manual ? (
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold">
|
||||
{t("moduleList.reorder.heading")}
|
||||
</h2>
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 3 }, (_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<ModuleOrderEditor items={reorderItems} onChange={setOrder} />
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold">
|
||||
{t("labs.biomarker.manageTitle")}
|
||||
</h2>
|
||||
<BiomarkerManager />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user