diff --git a/src/components/labs/lab-list.tsx b/src/components/labs/lab-list.tsx index 57b21e729..2f077bee4 100644 --- a/src/components/labs/lab-list.tsx +++ b/src/components/labs/lab-list.tsx @@ -211,7 +211,7 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {

) : null} - + {groups.map((group) => { const inner = (
@@ -219,10 +219,6 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) { {group.analyte} -
@@ -233,42 +229,53 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
); // The whole row navigates into the detail (where delete lives). - return ( + return group.biomarkerId ? ( + + {inner} + + +
+ reading.value)} + referenceLow={group.latest.referenceLow} + referenceHigh={group.latest.referenceHigh} + /> +
+ + + ) : ( + // Un-linked group (not backfilled to a catalog biomarker): + // render inert but say so, so it doesn't read as a broken + // link next to its clickable neighbours.
- {group.biomarkerId ? ( - - {inner} - -
- r.value)} - /> -
- - - ) : ( - // Un-linked group (not backfilled to a catalog biomarker): - // render inert but say so, so it doesn't read as a broken - // link next to its clickable neighbours. -
- {inner} - -
- r.value)} - /> -
- - {t("labs.notLinkedYet")} - -
- )} + {inner} + + +
+ reading.value)} + referenceLow={group.latest.referenceLow} + referenceHigh={group.latest.referenceHigh} + /> +
+ + {t("labs.notLinkedYet")} +
); })} @@ -359,7 +366,9 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) { /> r.value)} + values={group.readings.map((reading) => reading.value)} + referenceLow={group.latest.referenceLow} + referenceHigh={group.latest.referenceHigh} /> {/* Un-linked group: the header renders no link, so name the diff --git a/src/components/labs/lab-trend-sparkline.tsx b/src/components/labs/lab-trend-sparkline.tsx index 4633dc400..8d25e767a 100644 --- a/src/components/labs/lab-trend-sparkline.tsx +++ b/src/components/labs/lab-trend-sparkline.tsx @@ -13,9 +13,9 @@ * polyline vs Recharts) is used by the doctor-report PDF. The two are not * drift; do not unify them. * - * Rendered only when an analyte has ≥ 2 NUMERIC readings. Stroke uses the - * neutral `currentColor` so it inherits the calm muted-foreground tone of its - * row — no status colour, in keeping with the no-alarming-colour ethos. + * Rendered only when an analyte has ≥ 2 NUMERIC readings. The calm neutral + * line preserves the compact graph, while each reading uses the adjacent + * reference-range bar's semantic colour. * * `values` are passed oldest → newest. v1.18.9 — qualitative readings carry no * number (`null`); they are filtered out here so a series with qualitative @@ -23,10 +23,14 @@ */ export function LabTrendSparkline({ values: rawValues, + referenceLow = null, + referenceHigh = null, width = 72, height = 20, }: { values: (number | null)[]; + referenceLow?: number | null; + referenceHigh?: number | null; width?: number; height?: number; }) { @@ -40,32 +44,62 @@ export function LabTrendSparkline({ const pad = 2; const usableH = height - pad * 2; - const points = values - .map((v, i) => { - const x = i * stepX; - // Invert Y so a higher value sits higher on screen. - const y = pad + usableH - ((v - min) / span) * usableH; - return `${x.toFixed(1)},${y.toFixed(1)}`; - }) + const points = values.map((v, i) => { + const x = i * stepX; + // Invert Y so a higher value sits higher on screen. + const y = pad + usableH - ((v - min) / span) * usableH; + return { value: v, x, y }; + }); + const polylinePoints = points + .map(({ x, y }) => `${x.toFixed(1)},${y.toFixed(1)}`) .join(" "); + const isValidRange = + referenceLow !== null && + referenceHigh !== null && + referenceLow < referenceHigh; + const pointClassName = (value: number) => { + if (isValidRange) { + if (value < referenceLow) return "fill-info"; + if (value > referenceHigh) return "fill-warning"; + return "fill-success"; + } + if (referenceLow !== null) { + return value < referenceLow ? "fill-info" : "fill-success"; + } + if (referenceHigh !== null) { + return value > referenceHigh ? "fill-warning" : "fill-success"; + } + return "fill-muted-foreground"; + }; return ( + {points.map(({ value, x, y }, index) => ( + + ))} ); } diff --git a/src/components/labs/reference-range-badge.tsx b/src/components/labs/reference-range-badge.tsx index d48cad65a..4e59c1a8a 100644 --- a/src/components/labs/reference-range-badge.tsx +++ b/src/components/labs/reference-range-badge.tsx @@ -4,6 +4,7 @@ import { ArrowDown, ArrowUp, Minus } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { useTranslations } from "@/lib/i18n/context"; +import { cn } from "@/lib/utils"; import type { ReferenceRangeStatus } from "@/lib/validations/labs"; /** @@ -23,6 +24,7 @@ import type { ReferenceRangeStatus } from "@/lib/validations/labs"; export function ReferenceRangeBadge({ status, compact = false, + className, }: { status: ReferenceRangeStatus; /** @@ -31,6 +33,7 @@ export function ReferenceRangeBadge({ * The hero/detail view keeps the default (un-compact) size. */ compact?: boolean; + className?: string; }) { const { t } = useTranslations(); @@ -46,7 +49,7 @@ export function ReferenceRangeBadge({ return ( {compact ? null : } {t("labs.range.inRange")} @@ -64,7 +67,7 @@ export function ReferenceRangeBadge({ ? "border-info/30 bg-info/10 text-info" : "border-warning/30 bg-warning/10 text-warning"; return ( - + {label}