mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
Merge branch 'wave/v1221-d' into release/v1.22.1
This commit is contained in:
commit
acf4941be8
8 files changed
+266
-51
No files matched your search
@@ -11,6 +11,7 @@ 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 { resolveCatalogSlug } 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";
|
||||
@@ -77,6 +78,14 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
|
||||
const { t } = useTranslations();
|
||||
const { prefs } = useModuleListPrefs("labs");
|
||||
|
||||
// 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.
|
||||
const describeMarker = (analyte: string): string | null => {
|
||||
const slug = resolveCatalogSlug(analyte, (s) => t(`labs.catalog.${s}`));
|
||||
return slug ? t(`labs.catalog.desc.${slug}`) : null;
|
||||
};
|
||||
|
||||
const listKey = queryKeys.labResultsList({
|
||||
biomarkerId: undefined,
|
||||
analyte: undefined,
|
||||
@@ -183,6 +192,7 @@ 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">
|
||||
@@ -194,6 +204,11 @@ 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)}
|
||||
@@ -250,61 +265,69 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
|
||||
</p>
|
||||
</li>
|
||||
) : null}
|
||||
{groups.map((group) => (
|
||||
<li key={group.key} className="contents">
|
||||
<Card className="h-full gap-3">
|
||||
<MedicationCardHeader
|
||||
name={group.analyte}
|
||||
dose=""
|
||||
categoryLabel={group.panel ?? group.unit}
|
||||
nameChip={
|
||||
<ReferenceRangeBadge
|
||||
status={group.latest.rangeStatus}
|
||||
compact
|
||||
/>
|
||||
}
|
||||
href={
|
||||
group.biomarkerId ? `/labs/${group.biomarkerId}` : undefined
|
||||
}
|
||||
linkLabel={group.analyte}
|
||||
/>
|
||||
<CardContent>
|
||||
<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">
|
||||
{formatLabReading(group.latest)}
|
||||
</span>
|
||||
{group.latest.value !== null &&
|
||||
(group.latest.referenceLow !== null ||
|
||||
group.latest.referenceHigh !== null) ? (
|
||||
<span className="text-xs">
|
||||
{t("labs.referenceLabel")}{" "}
|
||||
{formatReferenceRange(
|
||||
group.latest.referenceLow,
|
||||
group.latest.referenceHigh,
|
||||
formatLabValue,
|
||||
)}
|
||||
{groups.map((group) => {
|
||||
const description = describeMarker(group.analyte);
|
||||
return (
|
||||
<li key={group.key} className="contents">
|
||||
<Card className="h-full gap-3">
|
||||
<MedicationCardHeader
|
||||
name={group.analyte}
|
||||
dose=""
|
||||
categoryLabel={group.panel ?? group.unit}
|
||||
nameChip={
|
||||
<ReferenceRangeBadge
|
||||
status={group.latest.rangeStatus}
|
||||
compact
|
||||
/>
|
||||
}
|
||||
href={
|
||||
group.biomarkerId ? `/labs/${group.biomarkerId}` : undefined
|
||||
}
|
||||
linkLabel={group.analyte}
|
||||
/>
|
||||
<CardContent>
|
||||
{description ? (
|
||||
<p className="text-muted-foreground mb-2 text-sm">
|
||||
{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">
|
||||
{formatLabReading(group.latest)}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-xs">
|
||||
{formatDate(group.latest.takenAt)}
|
||||
</span>
|
||||
{group.readings.length > 1 ? (
|
||||
{group.latest.value !== null &&
|
||||
(group.latest.referenceLow !== null ||
|
||||
group.latest.referenceHigh !== null) ? (
|
||||
<span className="text-xs">
|
||||
{t("labs.referenceLabel")}{" "}
|
||||
{formatReferenceRange(
|
||||
group.latest.referenceLow,
|
||||
group.latest.referenceHigh,
|
||||
formatLabValue,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-xs">
|
||||
{t("labs.readingsCount", {
|
||||
count: group.readings.length,
|
||||
})}
|
||||
{formatDate(group.latest.takenAt)}
|
||||
</span>
|
||||
) : null}
|
||||
{group.readings.length > 1 ? (
|
||||
<span className="text-xs">
|
||||
{t("labs.readingsCount", {
|
||||
count: group.readings.length,
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<LabTrendSparkline
|
||||
values={group.readings.map((r) => r.value)}
|
||||
/>
|
||||
</div>
|
||||
<LabTrendSparkline
|
||||
values={group.readings.map((r) => r.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -226,6 +226,30 @@ export const BIOMARKER_CATALOG: readonly BiomarkerSeed[] = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* v1.22 — resolve a reading's `analyte` (a display name) back to its catalog
|
||||
* slug so the Labs page can surface the marker's descriptive subtitle.
|
||||
*
|
||||
* The lab row stores the localised name the marker was created under, not the
|
||||
* slug. `localizedName(slug)` resolves the catalog name for the active locale
|
||||
* (`t("labs.catalog.<slug>")`); a case- and whitespace-insensitive match maps
|
||||
* the analyte to its slug. Free-text markers (and markers created under a
|
||||
* different locale than the one in view) return null, and the caller shows no
|
||||
* subtitle rather than a fabricated one.
|
||||
*/
|
||||
export function resolveCatalogSlug(
|
||||
analyte: string,
|
||||
localizedName: (slug: string) => string,
|
||||
): string | null {
|
||||
const norm = analyte.trim().toLowerCase();
|
||||
if (!norm) return null;
|
||||
for (const seed of BIOMARKER_CATALOG) {
|
||||
if (localizedName(seed.slug).trim().toLowerCase() === norm)
|
||||
return seed.slug;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The set of panel keys present in the catalog, in display order. */
|
||||
export const BIOMARKER_PANELS = [
|
||||
"lipids",
|
||||
|
||||
Reference in new issue
Block a user