refactor(labs): move the biomarker description to its detail page

The per-marker catalog description used to render on every labs overview
row, both in the list and the card layout. It reads better as a caption
beneath the detail-page heading, mirroring the metric pages' explainer
line. Resolve the catalog slug from the marker name and fall back to the
user's own context note.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 13:28:47 +02:00
1 parent d87aac4b48
commit f751f006de
2 files changed
+29 -34

No files matched your search

+27 -4
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { FlaskConical, Pencil, Plus } from "lucide-react";
@@ -13,6 +13,7 @@ import { EmptyState } from "@/components/ui/empty-state";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { apiDelete, apiGet } from "@/lib/api/api-fetch";
import { BIOMARKER_CATALOG } from "@/lib/labs/biomarker-catalog";
import { classifyReferenceRange } from "@/lib/labs/reference-range";
import { formatLabValue } from "@/lib/labs/format-value";
import { useTranslations } from "@/lib/i18n/context";
@@ -65,6 +66,18 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
const { t } = useTranslations();
const router = useRouter();
const queryClient = useQueryClient();
// v1.24 — the per-marker description used to live on the labs overview rows.
// It belongs on the detail page beneath the heading (mirroring the metric
// pages' explainer caption). Resolve the catalog slug from the marker name
// exactly as the overview did, then fall back to the user's own `context`.
const slugByName = useMemo(() => {
const map = new Map<string, string>();
for (const seed of BIOMARKER_CATALOG) {
const norm = t(`labs.catalog.${seed.slug}`).trim().toLowerCase();
if (norm) map.set(norm, seed.slug);
}
return map;
}, [t]);
const [addOpen, setAddOpen] = useState(false);
// Sticky-footer slot for the add-value sheet (the form portals here).
const [addFooterEl, setAddFooterEl] = useState<HTMLDivElement | null>(null);
@@ -223,9 +236,19 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
</div>
) : null}
{marker?.context ? (
<p className="text-muted-foreground text-sm">{marker.context}</p>
) : null}
{(() => {
const slug = marker?.name
? slugByName.get(marker.name.trim().toLowerCase())
: undefined;
const description = slug
? t(`labs.catalog.desc.${slug}`)
: (marker?.context ?? null);
return description ? (
<p className="text-muted-foreground text-sm leading-relaxed">
{description}
</p>
) : null;
})()}
<Card>
<CardContent className="pt-6">
+2 -30
View File
@@ -11,7 +11,6 @@ import { Card, CardContent } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
import { Skeleton } from "@/components/ui/skeleton";
import { apiGet } from "@/lib/api/api-fetch";
import { BIOMARKER_CATALOG } from "@/lib/labs/biomarker-catalog";
import { formatDate } from "@/lib/format";
import { formatReferenceRange } from "@/lib/labs/reference-range";
import { formatLabReading, formatLabValue } from "@/lib/labs/format-value";
@@ -81,23 +80,8 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
// v1.22 — a short, factual line under each marker heading describing what the
// biomarker measures, sourced from the catalog via i18n. Free-text markers
// (no catalog match) carry no subtitle rather than a fabricated one.
// v1.22.1 — build the localized name→slug map once per locale instead of
// re-scanning all catalog seeds (calling `t()` each) per group every render.
const slugByName = useMemo(() => {
const map = new Map<string, string>();
for (const seed of BIOMARKER_CATALOG) {
const norm = t(`labs.catalog.${seed.slug}`).trim().toLowerCase();
if (norm) map.set(norm, seed.slug);
}
return map;
}, [t]);
const describeMarker = (analyte: string): string | null => {
const norm = analyte.trim().toLowerCase();
if (!norm) return null;
const slug = slugByName.get(norm);
return slug ? t(`labs.catalog.desc.${slug}`) : null;
};
// v1.24 — the per-marker description moved to the biomarker detail page
// (beneath the heading); the overview rows no longer carry it.
const listKey = queryKeys.labResultsList({
biomarkerId: undefined,
analyte: undefined,
@@ -204,7 +188,6 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
<Card>
<CardContent className="divide-border divide-y p-0">
{groups.map((group) => {
const description = describeMarker(group.analyte);
const inner = (
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-baseline gap-x-2 text-sm">
@@ -216,11 +199,6 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
compact
/>
</div>
{description ? (
<p className="text-muted-foreground text-xs">
{description}
</p>
) : null}
<div className="text-muted-foreground flex flex-wrap items-center gap-x-2 text-xs">
<span className="text-foreground font-semibold tabular-nums">
{formatLabReading(group.latest)}
@@ -278,7 +256,6 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
</li>
) : null}
{groups.map((group) => {
const description = describeMarker(group.analyte);
return (
<li key={group.key} className="contents">
<Card className="h-full gap-3">
@@ -298,11 +275,6 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
linkLabel={group.analyte}
/>
<CardContent>
{description ? (
<p className="text-muted-foreground mb-2 text-xs">
{description}
</p>
) : null}
<div className="flex items-center justify-between gap-3">
<div className="text-muted-foreground flex flex-wrap items-center gap-x-2 text-sm">
<span className="text-foreground font-semibold tabular-nums">