fix(insights): plot the trends row and log fevers in the reader's units

Two more surfaces carried a fixed unit. The insights trends row labelled
weight in kg and body temperature in °C for every reader, and plotted
walking distance as raw metres under a km label; its trend caption
said the same. A single transformed series there now takes unit, scale
and offset from the preference, and the caption describes the converted
series against a converted noise floor.

The illness day sheet and timeline showed and took a fever in °C. The
fever is now seeded, typed and shown in the reader's temperature unit
and stored in °C as before.

Refs #1067
This commit is contained in:
Marc-André Bombeck committed 2026-10-01 09:08:11 +02:00
1 parent d6f79c8bef
commit bc2d60cce8
16 files changed
+229 -38

No files matched your search

@@ -0,0 +1,70 @@
/**
* A logged fever reads in the reader's temperature unit. The timeline and the
* day sheet printed a hardcoded °C, so an imperial account read its own
* fever in Celsius.
*/
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
const account = vi.hoisted(() => ({
user: { unitPreference: "imperial", glucoseUnit: null } as Record<
string,
unknown
>,
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({ user: account.user, isAuthenticated: true }),
useAccountOnceMounted: () => account.user,
}));
vi.mock("@/hooks/use-record-capabilities", () => ({
useRecordCapabilities: () => ({ canManageDomain: () => true }),
}));
vi.mock("../use-illness", () => ({
useIllnessDayLogList: () => ({
isLoading: false,
isError: false,
data: {
dayLogs: [
{
id: "d1",
episodeId: "e1",
date: "2026-09-30",
functionalImpact: null,
feverC: 38.5,
symptoms: [],
note: null,
updatedAt: "2026-09-30T08:00:00.000Z",
},
],
},
}),
}));
import { I18nProvider } from "@/lib/i18n/context";
import { IllnessDayTimeline } from "../illness-day-timeline";
function render(): string {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<IllnessDayTimeline episodeId="e1" onLogDay={() => {}} />
</I18nProvider>,
);
}
describe("<IllnessDayTimeline> fever unit", () => {
it("shows the fever in °F for an imperial reader", () => {
account.user = { unitPreference: "imperial", glucoseUnit: null };
const html = render();
// 38.5 °C = 101.3 °F.
expect(html).toContain("Temperature: 101.3 °F");
expect(html).not.toContain("°C");
});
it("keeps °C for a metric reader", () => {
account.user = { unitPreference: "metric", glucoseUnit: null };
expect(render()).toContain("Temperature: 38.5 °C");
});
});
@@ -19,6 +19,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { useUnitDisplay } from "@/hooks/use-unit-display";
import { ILLNESS_SYMPTOM_CATALOG } from "./symptom-catalog";
import { useIllnessDayLogList } from "./use-illness";
@@ -32,6 +33,7 @@ function symptomLabelKey(key: string): string | null {
function DayLogRow({ log }: { log: IllnessDayLogDTO }) {
const { t } = useTranslations();
const fmt = useFormatters();
const { toDisplay, unitFor } = useUnitDisplay();
return (
<div className="border-border/60 space-y-2 border-l-2 pl-3">
@@ -72,7 +74,10 @@ function DayLogRow({ log }: { log: IllnessDayLogDTO }) {
{log.feverC !== null ? (
<p className="text-foreground text-sm">
{t("illness.timeline.fever", { value: fmt.number(log.feverC, 1) })}
{t("illness.timeline.fever", {
value: fmt.number(toDisplay("BODY_TEMPERATURE", log.feverC), 1),
unit: unitFor("BODY_TEMPERATURE"),
})}
</p>
) : null}
+16 -4
View File
@@ -25,6 +25,7 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
import { useUnitDisplay } from "@/hooks/use-unit-display";
import { ILLNESS_SYMPTOM_CATALOG } from "./symptom-catalog";
import { useIllnessDayLog, useUpsertDayLog } from "./use-illness";
@@ -140,6 +141,10 @@ export function LogDaySheet({
onsetDate,
}: LogDaySheetProps) {
const { t } = useTranslations();
// The fever is stored in °C and typed in the reader's temperature unit:
// seeded through the transform, inverted on save.
const { toDisplay, fromDisplay, unitFor } = useUnitDisplay();
const feverUnit = unitFor("BODY_TEMPERATURE");
// The day being logged — seeded from `date` (today), but the picker lets the
// user backdate within [onset, today] for the retrospective journal.
@@ -178,7 +183,11 @@ export function LogDaySheet({
new Map((dto?.symptoms ?? []).map((s) => [s.key, s.severity ?? null])),
);
setFunctionalImpact(dto?.functionalImpact ?? null);
setFever(dto?.feverC != null ? String(dto.feverC) : "");
setFever(
dto?.feverC != null
? String(toDisplay("BODY_TEMPERATURE", dto.feverC))
: "",
);
setNote(dto?.note ?? "");
} else if (!open && lastFormKey !== null) {
setLastFormKey(null);
@@ -213,7 +222,10 @@ export function LogDaySheet({
const input: IllnessDayLogInput = {
date: selectedDate,
functionalImpact,
feverC: feverValue,
feverC:
feverValue == null
? null
: Math.round(fromDisplay("BODY_TEMPERATURE", feverValue) * 100) / 100,
symptoms: Array.from(symptoms.entries()).map(([key, severity]) => ({
key,
severity: severity ?? undefined,
@@ -333,7 +345,7 @@ export function LogDaySheet({
summary={
feverValue != null ? (
<span className="text-muted-foreground text-xs">
{feverValue.toFixed(1)} °C
{feverValue.toFixed(1)} {feverUnit}
</span>
) : null
}
@@ -345,7 +357,7 @@ export function LogDaySheet({
<Input
id="illness-fever"
inputMode="decimal"
placeholder="37.0"
placeholder={toDisplay("BODY_TEMPERATURE", 37).toFixed(1)}
value={fever}
onChange={(e) => setFever(e.target.value)}
className="max-w-32"