From a2f926576e801b13eb33096db8cf86f37ad3e261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 18 Jul 2026 03:49:15 +0200 Subject: [PATCH] feat(labs): add "save & add another" to the manual result form A real lab report is 10-20 analytes sharing one blood-draw date, usually in the past; the add form closed on every save, resetting takenAt to now and clearing the biomarker pick, so entering a full panel meant re-setting the date away from now on every single row. The new button keeps the sheet open, clears the reading, and deliberately preserves takenAt (and the biomarker pick when not locked to one detail page) so the next analyte in the same panel just needs a pick + a value. --- messages/de.json | 1 + messages/en.json | 1 + messages/es.json | 1 + messages/fr.json | 1 + messages/it.json | 1 + messages/pl.json | 1 + src/app/labs/page.tsx | 9 ++ src/components/labs/lab-biomarker-detail.tsx | 8 ++ src/components/labs/lab-form.tsx | 91 ++++++++++++++++++-- 9 files changed, 106 insertions(+), 8 deletions(-) diff --git a/messages/de.json b/messages/de.json index 96a0d94df..45a032940 100644 --- a/messages/de.json +++ b/messages/de.json @@ -7397,6 +7397,7 @@ "notePlaceholder": "Optionaler Kontext zu diesem Wert", "requiredError": "Bitte einen Wert eingeben.", "save": "Speichern", + "saveAndAddAnother": "Speichern & nächsten Wert", "savedToast": "Wert gespeichert", "saveError": "Wert konnte nicht gespeichert werden", "biomarker": "Biomarker", diff --git a/messages/en.json b/messages/en.json index ae8c7ee80..29fa9297c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7397,6 +7397,7 @@ "notePlaceholder": "Optional context for this result", "requiredError": "Enter a value.", "save": "Save", + "saveAndAddAnother": "Save & add another", "savedToast": "Result saved", "saveError": "Could not save the result", "biomarker": "Biomarker", diff --git a/messages/es.json b/messages/es.json index a011182c3..6eb0a7634 100644 --- a/messages/es.json +++ b/messages/es.json @@ -7397,6 +7397,7 @@ "notePlaceholder": "Contexto opcional para este resultado", "requiredError": "Introduce un valor.", "save": "Guardar", + "saveAndAddAnother": "Guardar y añadir otro", "savedToast": "Resultado guardado", "saveError": "No se pudo guardar el resultado", "biomarker": "Biomarcador", diff --git a/messages/fr.json b/messages/fr.json index 3c684a35b..75b440440 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -7397,6 +7397,7 @@ "notePlaceholder": "Contexte facultatif pour ce résultat", "requiredError": "Saisissez une valeur.", "save": "Enregistrer", + "saveAndAddAnother": "Enregistrer et ajouter un autre", "savedToast": "Résultat enregistré", "saveError": "Impossible d'enregistrer le résultat", "biomarker": "Biomarqueur", diff --git a/messages/it.json b/messages/it.json index 03bbf720a..af20a22e4 100644 --- a/messages/it.json +++ b/messages/it.json @@ -7397,6 +7397,7 @@ "notePlaceholder": "Contesto facoltativo per questo risultato", "requiredError": "Inserisci un valore.", "save": "Salva", + "saveAndAddAnother": "Salva e aggiungi un altro", "savedToast": "Risultato salvato", "saveError": "Impossibile salvare il risultato", "biomarker": "Biomarcatore", diff --git a/messages/pl.json b/messages/pl.json index c1e3ad683..09a3333d7 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -7397,6 +7397,7 @@ "notePlaceholder": "Opcjonalny kontekst tego wyniku", "requiredError": "Podaj wartość.", "save": "Zapisz", + "saveAndAddAnother": "Zapisz i dodaj kolejny", "savedToast": "Wynik zapisany", "saveError": "Nie udało się zapisać wyniku", "biomarker": "Biomarker", diff --git a/src/app/labs/page.tsx b/src/app/labs/page.tsx index 78545a18d..737a20f75 100644 --- a/src/app/labs/page.tsx +++ b/src/app/labs/page.tsx @@ -177,6 +177,15 @@ export default function LabsPage() { queryKey: queryKeys.labResults(), }); }} + // v1.30.1 H3 — "Save & add another" keeps the sheet open; the + // form clears the reading itself, this callback just refreshes + // the list underneath so the just-saved row is there once the + // user is done and closes the sheet. + onSavedKeepOpen={() => { + queryClient.invalidateQueries({ + queryKey: queryKeys.labResults(), + }); + }} onCancel={() => setDialogOpen(false)} /> diff --git a/src/components/labs/lab-biomarker-detail.tsx b/src/components/labs/lab-biomarker-detail.tsx index cddfed4e1..a86b522ea 100644 --- a/src/components/labs/lab-biomarker-detail.tsx +++ b/src/components/labs/lab-biomarker-detail.tsx @@ -230,6 +230,13 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) { queryClient.invalidateQueries({ queryKey: queryKeys.labResults() }); } + // v1.30.1 H3 — "Save & add another": same invalidation as `afterAdd`, + // minus the close. Useful here too — logging several past readings for + // this one biomarker back-to-back without re-opening the sheet each time. + function afterAddKeepOpen() { + queryClient.invalidateQueries({ queryKey: queryKeys.labResults() }); + } + function afterEditMarker() { setEditOpen(false); queryClient.invalidateQueries({ @@ -482,6 +489,7 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) { lockedBiomarkerId={biomarkerId} footerSlot={addFooterEl} onSuccess={afterAdd} + onSavedKeepOpen={afterAddKeepOpen} onCancel={() => setAddOpen(false)} /> diff --git a/src/components/labs/lab-form.tsx b/src/components/labs/lab-form.tsx index 4dd9792a9..381e8afa0 100644 --- a/src/components/labs/lab-form.tsx +++ b/src/components/labs/lab-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useId, useState } from "react"; +import { useId, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2 } from "lucide-react"; @@ -55,6 +55,17 @@ interface LabFormProps { lockedBiomarkerId?: string; onSuccess?: (created: LabResultDto) => void; onCancel?: () => void; + /** + * v1.30.1 (H3 QoL fix) — "Save & add another": the sheet stays open, the + * reading is cleared for the next entry, and `takenAt` is PRESERVED (a + * real lab report is 10-20 analytes sharing one blood-draw date, usually + * in the past — re-picking that date for every row was the pain point). + * Optional and additive: the second button only renders when the caller + * wires this up, so a consumer that never passes it keeps the single-Save + * form unchanged. The caller's only job is invalidating the list read — + * the form itself, not the caller, owns the "stay open + reset" behaviour. + */ + onSavedKeepOpen?: (created: LabResultDto) => void; /** * When mounted inside a `` the caller passes the sheet's * footer slot element here. The Cancel / Save action row is portalled into @@ -79,6 +90,7 @@ export function LabForm({ lockedBiomarkerId, onSuccess, onCancel, + onSavedKeepOpen, footerSlot, }: LabFormProps) { const { t } = useTranslations(); @@ -103,12 +115,19 @@ export function LabForm({ const [valueText, setValueText] = useState(""); const [takenAt, setTakenAt] = useState(defaultTakenAtValue); const [note, setNote] = useState(""); - const [submitting, setSubmitting] = useState(false); + // v1.30.1 H3 — which submit button is in flight, so the OTHER one doesn't + // also show a spinner. `!== null` replaces the old plain `submitting` + // boolean for every disabled check below. + const [pendingAction, setPendingAction] = useState< + "save" | "saveAndAddAnother" | null + >(null); + const submitting = pendingAction !== null; const [error, setError] = useState(null); const [defineOpen, setDefineOpen] = useState(false); const [defineFooterEl, setDefineFooterEl] = useState( null, ); + const biomarkerTriggerRef = useRef(null); const allMarkers = catalog?.biomarkers ?? []; const selected = allMarkers.find((m) => m.id === biomarkerId); @@ -132,10 +151,20 @@ export function LabForm({ setBiomarkerId(created.id); } - async function handleSubmit(e: React.FormEvent) { + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(null); + // v1.30.1 H3 — both Save and Save & add another are `type="submit"` + // buttons tied to this one `
` via the HTML `form` attribute (they + // render portalled into the sheet's footer, outside the `` tag + // itself); `submitter` distinguishes which one fired this submit. + const submitter = (e.nativeEvent as SubmitEvent) + .submitter as HTMLButtonElement | null; + const keepOpen = + onSavedKeepOpen != null && + submitter?.dataset.action === "save-and-add-another"; + if (!biomarkerId) { setError(t("labs.form.pickBiomarkerError")); return; @@ -159,7 +188,7 @@ export function LabForm({ } } - setSubmitting(true); + setPendingAction(keepOpen ? "saveAndAddAnother" : "save"); try { const created = await apiPost("/api/labs", { biomarkerId, @@ -170,13 +199,38 @@ export function LabForm({ ...(note.trim() ? { note: note.trim() } : {}), }); toast.success(t("labs.form.savedToast")); - onSuccess?.(created); + 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(""); + 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(); + } + } else { + onSuccess?.(created); + } } catch (err) { const message = err instanceof ApiError ? err.message : t("labs.form.saveError"); setError(message); } finally { - setSubmitting(false); + setPendingAction(null); } } @@ -200,8 +254,25 @@ export function LabForm({ {t("common.cancel")} ) : null} + {/* v1.30.1 H3 — additive: only renders when the caller wires up + `onSavedKeepOpen`. `data-action` is how `handleSubmit` tells this + button apart from the plain Save below via `event.submitter`. */} + {onSavedKeepOpen ? ( + + ) : null}