mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(visits): open an upcoming appointment where it lives
Visits are the second tab of `/checkups`, and the bare path opens the
first one, the preventive-care list, which never shows an appointment.
Every surface that listed an upcoming appointment linked it there: the
dashboard's next-appointment line, the Today rail item, the Today
overview fact and the appointment's push notification. A tap on the
appointment landed on the list it is not on.
Each now opens the visit itself (`/checkups?visit=<id>`), or the visits
tab (`/checkups?view=visits`) for a line naming several appointments.
The checkups page reads `view` to pick a tab. The rail item stops
borrowing the check-up item's action: it carries its own label
("View appointment") and intent `visit.view`. The appointment push names
its visit through the producer's `metadata.url` override; a check-up's
push keeps the checkups list.
The same mis-route sat on three links that lead to visits without
being upcoming: a condition's visit rows, the address book's back link
and the onboarding and timeline prompts to add a visit.
`visit-link-guard.test.ts` keeps the visit paths in the builder and
counts every bare `/checkups` per file, each with the reason it names
the checkups list.
This commit is contained in:
1 parent
78bacb0d86
commit
0dcafeeb70
29 files changed
+591
-23
No files matched your search
@@ -30,7 +30,8 @@
|
||||
* stands in its place — not an alarming empty card.
|
||||
*
|
||||
* Every `PriorityItem` action carries an `href` (dose.log → /medications,
|
||||
* sync.reconnect → /settings/integrations, checkup.view → /checkups), so
|
||||
* sync.reconnect → /settings/integrations, checkup.view → /checkups,
|
||||
* visit.view → the visit on the checkups page's visits tab), so
|
||||
* `PriorityCard` wires each tap as a `<Link>` to its existing destination
|
||||
* by construction; S2 invents no new backend action.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useEncounters } from "@/hooks/use-encounters";
|
||||
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
|
||||
import { visitPath } from "@/lib/encounters/visit-links";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import type { EncounterKind } from "@/generated/prisma/client";
|
||||
|
||||
@@ -95,7 +96,7 @@ export function EpisodeVisitsCard({ episodeId }: { episodeId: string }) {
|
||||
<li key={visit.id}>
|
||||
<ListRow asChild>
|
||||
<Link
|
||||
href="/checkups"
|
||||
href={visitPath(visit.id)}
|
||||
data-encounter-id={visit.id}
|
||||
className="hover:bg-muted/50 focus-visible:ring-ring/50 flex items-center gap-3 focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* The dashboard's next-appointment line opens the appointment, on the visits
|
||||
* tab, and not the checkups list the card otherwise summarises.
|
||||
*
|
||||
* The line linked the bare `/checkups`, which opens the preventive-care tab —
|
||||
* the one list an appointment is never on. The negative control is the card's
|
||||
* own empty state, whose link is to the checkups and stays there.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
const visit = {
|
||||
id: "visit-42",
|
||||
status: "PLANNED",
|
||||
kind: "SPECIALIST",
|
||||
// Inside the card's two-week horizon whenever the suite runs.
|
||||
occurredAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
practitioner: { name: "Practice Example" },
|
||||
};
|
||||
|
||||
vi.mock("@/hooks/use-measurement-reminders", () => ({
|
||||
useMeasurementReminders: () => ({
|
||||
data: [],
|
||||
isError: false,
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
useMeasurementReminderMutations: () => ({
|
||||
satisfy: { mutate: vi.fn(), isPending: false },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-encounters", () => ({
|
||||
useEncounters: () => ({ data: { upcoming: [visit], past: [] } }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-record-capabilities", () => ({
|
||||
useRecordCapabilities: () => ({
|
||||
canManage: true,
|
||||
canWriteDomain: () => true,
|
||||
canManageDomain: () => true,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
usePathname: () => "/",
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { VorsorgeDashboardCard } from "../vorsorge-dashboard-card";
|
||||
|
||||
function linkHref(html: string, slot: string): string | null {
|
||||
const tag = html.match(new RegExp(`<a[^>]*data-slot="${slot}"[^>]*>`));
|
||||
return tag?.[0].match(/href="([^"]*)"/)?.[1] ?? null;
|
||||
}
|
||||
|
||||
describe("<VorsorgeDashboardCard> — the next appointment", () => {
|
||||
it("opens the visit itself", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<VorsorgeDashboardCard />
|
||||
</I18nProvider>,
|
||||
);
|
||||
expect(html).toContain("Practice Example");
|
||||
expect(linkHref(html, "vorsorge-dashboard-next-visit")).toBe(
|
||||
"/checkups?visit=visit-42",
|
||||
);
|
||||
// The empty state names the checkups and still opens them.
|
||||
expect(html).toContain('href="/checkups"');
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ import { CalendarClock, CheckCircle2, Plus } from "lucide-react";
|
||||
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
|
||||
import { encounterKindText } from "@/components/encounters/encounter-labels";
|
||||
import { useEncounters } from "@/hooks/use-encounters";
|
||||
import { visitPath } from "@/lib/encounters/visit-links";
|
||||
import {
|
||||
useFormatters,
|
||||
useTranslations,
|
||||
@@ -240,10 +241,12 @@ export function VorsorgeDashboardCard() {
|
||||
|
||||
{/* Visually distinct from a due checkup: a quiet line under the block
|
||||
rather than a fourth row with an action, because there is nothing
|
||||
to do about an appointment except keep it. */}
|
||||
to do about an appointment except keep it. It opens the visit
|
||||
itself: a bare `/checkups` lands on the checkups tab, which is
|
||||
exactly where the appointment is not. */}
|
||||
{nextAppointment ? (
|
||||
<Link
|
||||
href="/checkups"
|
||||
href={visitPath(nextAppointment.id)}
|
||||
data-slot="vorsorge-dashboard-next-visit"
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 flex items-center gap-2 rounded-md border-t pt-2 text-xs transition-colors focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
usePractitioners,
|
||||
type Practitioner,
|
||||
} from "@/hooks/use-practitioners";
|
||||
import { VISITS_PATH } from "@/lib/encounters/visit-links";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { PractitionerSheet } from "./practitioner-sheet";
|
||||
|
||||
@@ -107,7 +108,7 @@ export function PractitionerList({ enabled = true }: { enabled?: boolean }) {
|
||||
<PageHeader
|
||||
title={t("practitioners.title")}
|
||||
description={t("practitioners.description")}
|
||||
backLink={{ href: "/checkups", label: t("encounters.sectionTitle") }}
|
||||
backLink={{ href: VISITS_PATH, label: t("encounters.sectionTitle") }}
|
||||
actions={addButton}
|
||||
/>
|
||||
|
||||
|
||||
Reference in new issue
Block a user