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}