fix(insights): take weight and BMI bands from the record on screen

Inside a shared or managed record the weight page shaded the band for
the viewer's own height, and the record's own target never arrived
because `/api/user/thresholds` refuses under a switch: a parent saw
their own healthy range behind a child's weights. The BMI page decided
from the viewer's height whether there was anything to show, and the
target gear opened an editor that could neither load nor save there.

The weight page now reads the record's target and height from
`/api/insights/targets`, the payload the reference panel under the
chart already uses, and paints the same wings as before. The BMI page
reads the record's height there inside somebody else's record. The
gear stays hidden in another person's record, as the BMI chart
already draws nothing there rather than a wrong figure.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 19:56:49 +01:00
1 parent 2c7a0dc3c2
commit 4fa5e6381e
10 files changed
+350 -43

No files matched your search

@@ -10,6 +10,12 @@ vi.mock("next/navigation", () => ({
usePathname: () => "/insights/weight",
}));
// The target gear asks whether this is somebody else's record; the shell
// renders here without the session it would read that from.
vi.mock("@/hooks/use-record-capabilities", () => ({
useRecordCapabilities: () => ({ inSharedRecord: false }),
}));
/**
* v1.4.25 — `<SubPageShell>` unit tests.
*
@@ -28,6 +28,11 @@ vi.mock("@/lib/insights/target-adjust-context", () => ({
useTargetAdjust: () => useTargetAdjustMock(),
}));
const capabilities = { inSharedRecord: false };
vi.mock("@/hooks/use-record-capabilities", () => ({
useRecordCapabilities: () => capabilities,
}));
const { TargetAdjustButton } = await import("../target-adjust-button");
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
@@ -53,6 +58,22 @@ describe("<TargetAdjustButton>", () => {
expect(html).toBe("");
});
it("renders nothing inside somebody else's record", () => {
// The editor reads and writes the signed-in account's own targets, which
// the server refuses under a switch: the gear would open a dead sheet.
useTargetAdjustMock.mockReturnValue({
canAdjust: true,
requestAdjust: vi.fn(),
register: vi.fn(),
});
capabilities.inSharedRecord = true;
try {
expect(render(<TargetAdjustButton />)).toBe("");
} finally {
capabilities.inSharedRecord = false;
}
});
it("renders the gear once an editable target is registered", () => {
useTargetAdjustMock.mockReturnValue({
canAdjust: true,
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
import { useTargetAdjust } from "@/lib/insights/target-adjust-context";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
/**
* Header gear that opens the per-metric target editor.
@@ -20,6 +21,10 @@ import { useTargetAdjust } from "@/lib/insights/target-adjust-context";
* has registered for the page (e.g. a metric without a numeric
* target band, where the card paints nothing), so the header never
* shows a dead control;
* - renders nothing inside somebody else's record: the editor loads and
* saves the signed-in account's own targets (`/api/user/thresholds`),
* which refuses under a switch, so the gear would open a sheet that
* can do neither;
* - opens the primary registered target's `<TargetEditSheet>` on click.
*
* Sized to match `<CoachLaunchButton variant="icon">` so the gear + the
@@ -29,9 +34,11 @@ import { useTargetAdjust } from "@/lib/insights/target-adjust-context";
export function TargetAdjustButton({ className }: { className?: string }) {
const { t } = useTranslations();
const adjust = useTargetAdjust();
const { inSharedRecord } = useRecordCapabilities();
// No provider, or no editable target registered for this page.
if (!adjust || !adjust.canAdjust) return null;
// No provider, no editable target registered for this page, or a record
// whose targets this editor cannot reach.
if (!adjust || !adjust.canAdjust || inSharedRecord) return null;
// Reuse the action's existing copy ("Adjust target range" /
// "Zielbereich anpassen") — the gear performs exactly what the old