fix(labs): keep the biomarker picked after "Save and add another" (#1029)

The reset after "Save and add another" cleared the biomarker and passed
the picker `undefined`, which switched it to its own internal state. It
went on showing the marker it last held while the form value was empty,
so the next save refused with "Pick a biomarker first" under a visibly
picked marker, and a later pick could land on the first catalogue entry.

The picker is always controlled now (an empty pick shows the
placeholder), and "Save and add another" keeps the marker, the draw date
and the visit, clearing only the reading, its note and its printed
range. Focus moves to the value field for the next entry.
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 08:02:11 +02:00
1 parent e9e060083c
commit cb12882aff
2 files changed
+108 -30

No files matched your search

@@ -0,0 +1,44 @@
/**
* "Save & add another" keeps the report and the marker, clears the reading
* (#1029).
*
* It used to clear the biomarker too. The picker then fell out of its
* controlled state and went on showing the marker it last held while the
* form's own value was empty, so the second save answered "Pick a biomarker
* first" under a visibly picked marker. Keeping the marker removes the reset
* that caused the disagreement; the picker itself is now always controlled,
* so an empty pick shows the placeholder instead of a stale marker.
*/
import { describe, it, expect } from "vitest";
import { nextEntryAfterKeepOpen, type LabEntryDraft } from "../lab-form";
const saved: LabEntryDraft = {
biomarkerId: "bm-vitamin-d",
value: "42",
valueText: "",
takenAt: "2026-09-01T08:30",
note: "fasting",
sourceRange: "30 - 100",
visitId: "visit-1",
};
describe("nextEntryAfterKeepOpen", () => {
it("keeps the picked biomarker so the next save does not ask for it again", () => {
expect(nextEntryAfterKeepOpen(saved).biomarkerId).toBe("bm-vitamin-d");
});
it("keeps what the report shares: its draw date and its visit", () => {
const next = nextEntryAfterKeepOpen(saved);
expect(next.takenAt).toBe("2026-09-01T08:30");
expect(next.visitId).toBe("visit-1");
});
it("clears the reading itself and its printed window", () => {
const next = nextEntryAfterKeepOpen({ ...saved, valueText: "negative" });
expect(next.value).toBe("");
expect(next.valueText).toBe("");
expect(next.note).toBe("");
expect(next.sourceRange).toBe("");
});
});
+64 -30
View File
@@ -1,7 +1,7 @@
"use client";
import { useActiveRecordName } from "@/hooks/use-record-capabilities";
import { useId, useMemo, useRef, useState } from "react";
import { useId, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
@@ -57,6 +57,40 @@ function parseDecimal(raw: string): number | null {
return Number.isFinite(n) ? n : null;
}
/** The per-reading fields "Save & add another" decides to keep or clear. */
export interface LabEntryDraft {
biomarkerId: string;
value: string;
valueText: string;
takenAt: string;
note: string;
sourceRange: string;
visitId: string | null;
}
/**
* The form after "Save & add another": the reading is cleared, everything that
* describes the report it came from stays. The draw date and the visit are
* shared by every value on one report, and the biomarker stays because a run
* of entries is as often one marker over several dates as several markers on
* one date; re-picking it each time is what the button exists to avoid.
*
* Clearing the biomarker here used to drop the picker out of its controlled
* state. It kept showing the marker it last held while the form value was
* empty, so the next save refused with "Pick a biomarker first" under a
* visibly picked marker.
*/
export function nextEntryAfterKeepOpen(draft: LabEntryDraft): LabEntryDraft {
return {
...draft,
value: "",
valueText: "",
note: "",
// The next reading has its own printed window.
sourceRange: "",
};
}
interface LabFormProps {
/** When set, the biomarker is pre-selected and locked (add-from-detail). */
lockedBiomarkerId?: string;
@@ -157,7 +191,6 @@ export function LabForm({
const [defineFooterEl, setDefineFooterEl] = useState<HTMLDivElement | null>(
null,
);
const biomarkerTriggerRef = useRef<HTMLButtonElement | null>(null);
const allMarkers = catalog?.biomarkers ?? [];
const selected = allMarkers.find((m) => m.id === biomarkerId);
@@ -256,30 +289,31 @@ export function LabForm({
: undefined,
);
if (keepOpen) {
// A real lab report shares one blood-draw date across every
// analyte — clear the reading but deliberately KEEP `takenAt` so
// the next row doesn't need it re-set away from "now" again.
// The biomarker resets too (unless locked to one detail page)
// since the whole point is entering the NEXT analyte.
if (!lockedBiomarkerId) setBiomarkerId("");
setValue("");
setValueText("");
setNote("");
// The next analyte off the same report has its own printed window.
setSourceRange("");
const next = nextEntryAfterKeepOpen({
biomarkerId,
value,
valueText,
takenAt,
note,
sourceRange,
visitId,
});
setBiomarkerId(next.biomarkerId);
setValue(next.value);
setValueText(next.valueText);
setTakenAt(next.takenAt);
setNote(next.note);
setSourceRange(next.sourceRange);
setVisitId(next.visitId);
setError(null);
onSavedKeepOpen?.(created);
// Return focus to wherever the next entry starts so the flow
// stays keyboard/screen-reader friendly across repeated saves.
if (!lockedBiomarkerId) {
biomarkerTriggerRef.current?.focus();
} else {
document
.getElementById(
resultType === "numeric" ? "lab-value" : "lab-valueText",
)
?.focus();
}
// The next entry starts at the value, so focus goes there and keeps
// the flow keyboard/screen-reader friendly across repeated saves.
document
.getElementById(
resultType === "numeric" ? "lab-value" : "lab-valueText",
)
?.focus();
} else {
onSuccess?.(created);
}
@@ -355,16 +389,16 @@ export function LabForm({
<form id={formId} onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="lab-biomarker">{t("labs.form.biomarker")}</Label>
{/* Always controlled: `""` shows the placeholder. Passing
`undefined` for an empty pick switched the picker to its own
internal state, which kept displaying the last marker while the
form held none. */}
<Select
value={biomarkerId || undefined}
value={biomarkerId}
onValueChange={handleSelect}
disabled={!!lockedBiomarkerId || catalogLoading}
>
<SelectTrigger
id="lab-biomarker"
ref={biomarkerTriggerRef}
className="w-full"
>
<SelectTrigger id="lab-biomarker" className="w-full">
<SelectValue placeholder={t("labs.form.biomarkerPlaceholder")} />
</SelectTrigger>
<SelectContent>