mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +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
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Which tab a link to `/checkups` opens.
|
||||
*
|
||||
* Visits are the page's second tab. `?visit=<id>` opens one, `?view=visits`
|
||||
* the list, and the bare path keeps opening the checkups, which is what the
|
||||
* nav entry and the checkup surfaces mean by it.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
let search = new URLSearchParams();
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
usePathname: () => "/checkups",
|
||||
useSearchParams: () => search,
|
||||
}));
|
||||
vi.mock("@tanstack/react-query", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@tanstack/react-query")>()),
|
||||
useQueryClient: () => ({}),
|
||||
}));
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({ isAuthenticated: true, isLoading: false }),
|
||||
}));
|
||||
vi.mock("@/hooks/use-mounted", () => ({ useMounted: () => true }));
|
||||
vi.mock("@/hooks/use-pull-to-refresh", () => ({
|
||||
usePullToRefresh: () => ({}),
|
||||
}));
|
||||
vi.mock("@/components/ui/pull-to-refresh-indicator", () => ({
|
||||
PullToRefreshIndicator: () => null,
|
||||
}));
|
||||
vi.mock("@/components/measurement-reminders/vorsorge-section", () => ({
|
||||
VorsorgeSection: () => <div data-section="vorsorge" />,
|
||||
}));
|
||||
vi.mock("@/components/encounters/visits-section", () => ({
|
||||
VisitsSection: () => <div data-section="visits" />,
|
||||
}));
|
||||
vi.mock("@/components/encounters/procedures-section", () => ({
|
||||
ProceduresSection: () => <div data-section="procedures" />,
|
||||
}));
|
||||
vi.mock("@/components/body-sites/body-sites-section", () => ({
|
||||
BodySitesSection: () => <div data-section="body-sites" />,
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import CheckupsPageClient from "../page-client";
|
||||
|
||||
function openTab(query: string): string | null {
|
||||
search = new URLSearchParams(query);
|
||||
const html = renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<CheckupsPageClient />
|
||||
</I18nProvider>,
|
||||
);
|
||||
return html.match(/data-section="([^"]+)"/)?.[1] ?? null;
|
||||
}
|
||||
|
||||
describe("/checkups — the tab a link opens", () => {
|
||||
it("opens the checkups on the bare path", () => {
|
||||
expect(openTab("")).toBe("vorsorge");
|
||||
});
|
||||
|
||||
it("opens the visits for a link to one visit", () => {
|
||||
expect(openTab("visit=v1")).toBe("visits");
|
||||
});
|
||||
|
||||
it("opens the tab `view` names", () => {
|
||||
expect(openTab("view=visits")).toBe("visits");
|
||||
expect(openTab("view=procedures")).toBe("procedures");
|
||||
});
|
||||
|
||||
it("ignores a `view` it does not know", () => {
|
||||
expect(openTab("view=elsewhere")).toBe("vorsorge");
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import { VisitsSection } from "@/components/encounters/visits-section";
|
||||
import { ProceduresSection } from "@/components/encounters/procedures-section";
|
||||
import { BodySitesSection } from "@/components/body-sites/body-sites-section";
|
||||
import type { BodySiteSide } from "@/hooks/use-body-sites";
|
||||
import { parseCheckupsView } from "@/lib/encounters/visit-links";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, queryKeys } from "@/lib/query-keys";
|
||||
|
||||
@@ -66,17 +67,29 @@ export default function CheckupsPageClient() {
|
||||
linkedSideParam === "BOTH"
|
||||
? linkedSideParam
|
||||
: null;
|
||||
const [view, setView] = useState(
|
||||
linkedVisit ? "visits" : linkedSite ? "body-sites" : "vorsorge",
|
||||
// A link to one tab (`?view=visits`, from a line naming several
|
||||
// appointments). Built by `src/lib/encounters/visit-links.ts`, never by hand.
|
||||
const linkedView = parseCheckupsView(searchParams?.get("view"));
|
||||
const [view, setView] = useState<string>(
|
||||
linkedVisit
|
||||
? "visits"
|
||||
: linkedSite
|
||||
? "body-sites"
|
||||
: (linkedView ?? "vorsorge"),
|
||||
);
|
||||
// A link to a visit can arrive while the page is already open (the search
|
||||
// opens one from wherever the person is): it moves to the visits tab then
|
||||
// too, rather than only on the first render.
|
||||
// too, rather than only on the first render. A link to a tab does the same.
|
||||
const [observedVisit, setObservedVisit] = useState(linkedVisit);
|
||||
if (linkedVisit !== observedVisit) {
|
||||
setObservedVisit(linkedVisit);
|
||||
if (linkedVisit) setView("visits");
|
||||
}
|
||||
const [observedView, setObservedView] = useState(linkedView);
|
||||
if (linkedView !== observedView) {
|
||||
setObservedView(linkedView);
|
||||
if (linkedView && !linkedVisit) setView(linkedView);
|
||||
}
|
||||
|
||||
const refresh = useCallback(
|
||||
() =>
|
||||
|
||||
Reference in new issue
Block a user