fix(visits): keep the since-last-visit card on a failed read

A failed read replaced the card and its title with a bare row, so the
section changed height and the reader lost what had failed. The card now
keeps its header and shows the error inside. The visit card's header
moves onto TileHeader instead of its own icon and title row.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 10:10:36 +02:00
1 parent 3fbd16a0a6
commit a1b56f0b75
4 files changed
+43 -26

No files matched your search

+23 -7
View File
@@ -194,14 +194,31 @@ export function SinceLastVisit({
})
: [];
const lastLabel = fmt.dateShortSmartCalendar(lastDay);
// A failed read keeps the card and its title, so the page does not jump
// and the reader still sees what could not be loaded.
if (window.isError && rows.length === 0) {
return (
<QueryErrorRow
error={window.error}
slot="since-last-visit-error"
message={t("day.sinceLastVisit.loadError")}
onRetry={() => void window.refetch()}
/>
<Card className="gap-2 py-3 md:py-4">
<CardHeader>
<TileHeader
size="sm"
icon={Stethoscope}
titleAs="h3"
title={t("day.sinceLastVisit.title", { date: lastLabel })}
/>
</CardHeader>
<CardContent>
<QueryErrorRow
inset
error={window.error}
slot="since-last-visit-error"
message={t("day.sinceLastVisit.loadError")}
onRetry={() => void window.refetch()}
/>
</CardContent>
</Card>
);
}
// Nothing to show is nothing drawn: still loading, or a quiet window. The
@@ -209,7 +226,6 @@ export function SinceLastVisit({
if (rows.length === 0) return null;
const visible = showAll ? rows : rows.slice(0, VISIBLE_ROWS);
const lastLabel = fmt.dateShortSmartCalendar(lastDay);
const nextDay = dateKeyOfInstant(next.occurredAt, timeZone);
return (
@@ -164,7 +164,7 @@ describe("<ProceduresSection>", () => {
bodySites: [],
total: 1,
});
expect(html).toMatch(/font-medium">Meniscus repair</);
expect(html).toMatch(/card-title"[^>]*>Meniscus repair</);
expect(html).toContain("Sample clinic");
expect(html.match(/Meniscus repair/g)).toHaveLength(1);
});
@@ -261,7 +261,10 @@ describe("<VisitsSection> since the last visit", () => {
expect(html).toContain('data-slot="since-last-visit-error"');
expect(html).toContain('role="alert"');
expect(html).not.toContain('data-slot="since-last-visit"');
expect(upcomingCards(html)).toBe(upcomingCards(baseline));
// The failed window keeps its own card and title, so the page does not
// jump; the visit cards beside it stay as they were.
expect(upcomingCards(html)).toBe(upcomingCards(baseline) + 1);
expect(html).toContain("Since the last visit on");
});
// Visits need the profile section, the window reads measurements, and a
+15 -17
View File
@@ -28,6 +28,7 @@ import {
encounterKindText,
encounterStatusText,
} from "./encounter-labels";
import { TileHeader } from "@/components/insights/tile-header";
/** The two statuses worth calling out on a row; the rest the section implies. */
function isNoteworthy(status: EncounterStatus): boolean {
@@ -97,23 +98,20 @@ export function VisitCard({
>
<CardContent className="space-y-2">
<Body onOpen={onOpen} encounter={encounter}>
<div className="flex items-start gap-2">
<CalendarClock
className="text-foreground mt-0.5 size-5 shrink-0"
aria-hidden
/>
<div className="min-w-0 flex-1">
<p className="text-foreground truncate text-sm font-medium">
{heading}
</p>
<p className="text-muted-foreground text-xs">
{format.dateTime(encounter.occurredAt)}
</p>
</div>
<Badge variant="outline" className="shrink-0">
{encounterKindText(t, encounter.kind as EncounterKind)}
</Badge>
</div>
<TileHeader
size="sm"
icon={CalendarClock}
title={heading}
titleClassName="min-w-0 truncate"
right={
<Badge variant="outline" className="shrink-0">
{encounterKindText(t, encounter.kind as EncounterKind)}
</Badge>
}
/>
<p className="text-muted-foreground text-xs">
{format.dateTime(encounter.occurredAt)}
</p>
{reasonLine ? (
<p className="text-foreground line-clamp-1 text-sm">{reasonLine}</p>