diff --git a/messages/de.json b/messages/de.json index c07f8cf1f..5db331c6c 100644 --- a/messages/de.json +++ b/messages/de.json @@ -6720,7 +6720,14 @@ "savedToast": "Biomarker gespeichert", "updatedToast": "Biomarker aktualisiert", "saveError": "Biomarker konnte nicht gespeichert werden" - } + }, + "hide": "Ausblenden", + "unhide": "Wiederherstellen", + "hiddenToast": "Biomarker ausgeblendet", + "unhiddenToast": "Biomarker wiederhergestellt", + "hideError": "Biomarker konnte nicht aktualisiert werden", + "allHidden": "Alle Biomarker sind ausgeblendet.", + "hiddenSection": "Ausgeblendet" }, "catalog": { "total-cholesterol": "Gesamtcholesterin", diff --git a/messages/en.json b/messages/en.json index 4e8e9c6f5..cf1ac2738 100644 --- a/messages/en.json +++ b/messages/en.json @@ -6720,7 +6720,14 @@ "savedToast": "Biomarker saved", "updatedToast": "Biomarker updated", "saveError": "Could not save the biomarker" - } + }, + "hide": "Hide", + "unhide": "Restore", + "hiddenToast": "Biomarker hidden", + "unhiddenToast": "Biomarker restored", + "hideError": "Could not update the biomarker", + "allHidden": "Every biomarker is hidden.", + "hiddenSection": "Hidden" }, "catalog": { "total-cholesterol": "Total cholesterol", diff --git a/src/components/labs/biomarker-manager.tsx b/src/components/labs/biomarker-manager.tsx index 1bcfce316..8608c0a8c 100644 --- a/src/components/labs/biomarker-manager.tsx +++ b/src/components/labs/biomarker-manager.tsx @@ -2,16 +2,15 @@ import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { FlaskConical, Pencil, Plus } from "lucide-react"; +import { Eye, EyeOff, FlaskConical, Pencil, Plus } from "lucide-react"; import { toast } from "sonner"; import { DeleteButton } from "@/components/data-list"; import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; import { EmptyState } from "@/components/ui/empty-state"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { Skeleton } from "@/components/ui/skeleton"; -import { apiDelete, apiGet } from "@/lib/api/api-fetch"; +import { apiDelete, apiGet, apiPut } from "@/lib/api/api-fetch"; import { formatReferenceRange } from "@/lib/labs/reference-range"; import { formatLabValue } from "@/lib/labs/format-value"; import { useTranslations } from "@/lib/i18n/context"; @@ -21,12 +20,18 @@ import { BiomarkerForm } from "./biomarker-form"; import type { BiomarkerDto, BiomarkerListResponse } from "./types"; /** - * v1.18.1 — the Biomarker catalog manager. + * v1.18.1 — the Biomarker catalog manager. v1.22 — compact editable rows. * * Lives on the Labs page (not Settings) because the catalog IS the Labs - * feature's primary object. Lists every defined marker with its unit + - * reference range, opens a define-new / edit sheet, and wires delete (the - * `onDelete: SetNull` FK keeps existing readings, just unlinks them). + * feature's primary object. Each marker renders as a single compact row + * (name + unit inline, reference range / panel on a muted sub-line) with a + * hide toggle, an edit button (reusing the define/edit sheet), and a delete + * (the `onDelete: SetNull` FK keeps existing readings, just unlinks them). + * + * "Hide" is a soft remove: a marker the user no longer needs leaves the + * active list and the lab-entry pickers but keeps its readings + canonical + * unit/range. Hidden markers collect in a separate section where they can be + * restored or deleted outright. */ export function BiomarkerManager() { const { t } = useTranslations(); @@ -50,6 +55,20 @@ export function BiomarkerManager() { onError: () => toast.error(t("labs.biomarker.deleteError")), }); + const hiddenMutation = useMutation({ + mutationFn: ({ id, hidden }: { id: string; hidden: boolean }) => + apiPut(`/api/biomarkers/${id}`, { hidden }), + onSuccess: (_data, { hidden }) => { + toast.success( + hidden + ? t("labs.biomarker.hiddenToast") + : t("labs.biomarker.unhiddenToast"), + ); + queryClient.invalidateQueries({ queryKey: queryKeys.biomarkers() }); + }, + onError: () => toast.error(t("labs.biomarker.hideError")), + }); + function openNew() { setEditing(null); setFormOpen(true); @@ -68,6 +87,73 @@ export function BiomarkerManager() { } const markers = data?.biomarkers ?? []; + const visible = markers.filter((m) => !m.hidden); + const hidden = markers.filter((m) => m.hidden); + + function renderRow(marker: BiomarkerDto) { + const range = formatReferenceRange( + marker.lowerBound, + marker.upperBound, + formatLabValue, + ); + return ( +
  • +
    +
    + {marker.name} + {marker.unit} +
    +

    + {range + ? `${t("labs.referenceLabel")} ${range} ${marker.unit}` + : t("labs.biomarker.noRange")} + {marker.panel ? ` · ${marker.panel}` : ""} +

    +
    + + + deleteMutation.mutate(marker.id)} + title={t("labs.biomarker.deleteConfirmTitle")} + description={t("labs.biomarker.deleteConfirmDescription")} + confirmLabel={t("labs.biomarker.delete")} + className="size-11 sm:size-9" + iconClassName="h-4 w-4" + /> +
  • + ); + } return (
    @@ -84,7 +170,7 @@ export function BiomarkerManager() { {isLoading ? (
    {Array.from({ length: 3 }, (_, i) => ( - + ))}
    ) : isError ? ( @@ -101,60 +187,24 @@ export function BiomarkerManager() { } /> ) : ( - +
    + {visible.length > 0 ? ( + + ) : ( +

    + {t("labs.biomarker.allHidden")} +

    + )} + + {hidden.length > 0 ? ( +
    +

    + {t("labs.biomarker.hiddenSection")} +

    +
      {hidden.map(renderRow)}
    +
    + ) : null} +
    )} m.id === biomarkerId); + const allMarkers = catalog?.biomarkers ?? []; + const selected = allMarkers.find((m) => m.id === biomarkerId); + // v1.22 — hidden markers drop from the picker, but keep the currently + // selected one visible (e.g. editing a reading whose marker was later + // hidden) so the Select still resolves its label. + const markers = allMarkers.filter((m) => !m.hidden || m.id === biomarkerId); function handleSelect(next: string) { if (next === DEFINE_NEW) {