Files
HealthLog/src/components/labs/lab-values-list.tsx
T
Marc-André Bombeck 9a35cfb3e9 fix(ui): route bare read-failure branches through QueryErrorCard
Eight data surfaces still rendered a read failure as a bare
text-destructive paragraph with no retry: the biomarker manager, the lab
values list, the three record managers (allergies, family history,
conditions), the illness episode detail, and the intake history table;
the lab biomarker detail carried an inline near-duplicate of the
primitive from before it existed. Replace all of them with
QueryErrorCard wired to the query's refetch, keeping each surface's own
error string. Inline role="alert" text remains the pattern for mutation
errors inside forms.
2026-07-05 16:33:10 +02:00

116 lines
3.4 KiB
TypeScript

"use client";
import { useMemo } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { Skeleton } from "@/components/ui/skeleton";
import { apiGet } from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { LabHistoryList } from "./lab-history-list";
import type { LabResultDto, LabResultListResponse } from "./types";
// Page size for the offset-paginated reading feed. The server caps a single
// read at 500 (`listLabResultsSchema`); this stays well under that and keeps
// the first paint light, with "Load more" pulling subsequent pages.
const READINGS_PAGE_SIZE = 200;
/**
* v1.25.1 — the full reading history for one biomarker, lifted off the detail
* page onto its own `/labs/[biomarkerId]/values` route (mirroring the metric
* sub-pages' `/insights/values/<type>` split). The detail page keeps the
* numbers-first spine; this is the raw, editable reading feed with "Load more".
* Reuses the same offset-paginated infinite query key as the detail page so the
* two share cache.
*/
export function LabValuesList({ biomarkerId }: { biomarkerId: string }) {
const { t } = useTranslations();
const {
data: list,
isLoading,
isError,
refetch,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: queryKeys.labResultsInfinite({ biomarkerId, sortDir: "desc" }),
initialPageParam: 0,
queryFn: ({ pageParam }) =>
apiGet<LabResultListResponse>(
`/api/labs?biomarkerId=${encodeURIComponent(
biomarkerId,
)}&limit=${READINGS_PAGE_SIZE}&offset=${pageParam}&sortDir=desc`,
),
getNextPageParam: (lastPage) => {
const next = lastPage.meta.offset + lastPage.meta.limit;
return next < lastPage.meta.total ? next : undefined;
},
});
const readings: LabResultDto[] = useMemo(
() => list?.pages.flatMap((p) => p.results) ?? [],
[list],
);
const total = list?.pages[0]?.meta.total ?? 0;
const truncated = hasNextPage ?? false;
if (isError) {
return (
<QueryErrorCard
title={t("labs.loadError")}
onRetry={() => void refetch()}
/>
);
}
if (isLoading) {
return <Skeleton className="h-60 w-full" />;
}
if (readings.length === 0) {
return (
<p className="text-muted-foreground py-8 text-center text-sm">
{t("labs.detail.emptyTitle")}
</p>
);
}
return (
<div className="space-y-2">
{truncated ? (
<p className="text-muted-foreground text-xs">
{t("labs.showingLatestOf", {
shown: readings.length,
total,
})}
</p>
) : null}
<Card>
<CardContent className="py-0">
<LabHistoryList readings={readings} />
</CardContent>
</Card>
{hasNextPage ? (
<div className="flex justify-center">
<Button
variant="outline"
className="min-h-11 sm:min-h-9"
disabled={isFetchingNextPage}
onClick={() => fetchNextPage()}
>
{isFetchingNextPage
? t("common.loading")
: t("labs.loadMoreReadings")}
</Button>
</div>
) : null}
</div>
);
}