diff --git a/src/components/documents/document-record-links.tsx b/src/components/documents/document-record-links.tsx index 93b4c4509..c3cbd6827 100644 --- a/src/components/documents/document-record-links.tsx +++ b/src/components/documents/document-record-links.tsx @@ -50,6 +50,7 @@ import { queryKeys } from "@/lib/query-keys"; import type { EncounterDTO, EncounterListDTO } from "@/lib/encounters/dto"; import type { EncounterKind } from "@/generated/prisma/client"; import type { InboundDocumentDetailDto } from "@/lib/validations/inbound-documents"; +import { visitPath } from "@/lib/encounters/visit-links"; type Translate = ReturnType["t"]; @@ -109,7 +110,7 @@ export async function fetchEncounterPage( /** Where a visit opens: its sheet on the checkups page. */ function visitHref(encounterId: string): string { - return `/checkups?visit=${encodeURIComponent(encounterId)}`; + return visitPath(encounterId); } /** Where a dose opens: its sheet on the vaccinations page. */ diff --git a/src/components/links/related-records.tsx b/src/components/links/related-records.tsx index aadcfd923..85ff071f4 100644 --- a/src/components/links/related-records.tsx +++ b/src/components/links/related-records.tsx @@ -64,6 +64,7 @@ import type { IllnessEpisodeDTO } from "@/lib/illness/dto"; import type { RelatedLink } from "@/lib/links/related"; import type { EncounterKind } from "@/generated/prisma/client"; import { translateOrNull } from "@/lib/i18n/translate-code"; +import { visitPath } from "@/lib/encounters/visit-links"; type Translate = ReturnType["t"]; type RelatedKind = RelatedLink["kind"]; @@ -311,7 +312,7 @@ function VisitPicker({ selected, onChange, anchor, known }: PickerProps) { label: encounterKindText(t, visit.kind as EncounterKind), meta: visit.practitioner?.name ?? null, occurredAt: visit.occurredAt, - href: `/checkups?visit=${encodeURIComponent(visit.id)}`, + href: visitPath(visit.id), })), anchor, { diff --git a/src/lib/day/records.ts b/src/lib/day/records.ts index d4794e2a8..b3581cac9 100644 --- a/src/lib/day/records.ts +++ b/src/lib/day/records.ts @@ -41,6 +41,7 @@ import { resolveCycleDay } from "@/lib/cycle/verdict"; import { effectiveMoodTz } from "@/lib/mood/date-key"; import { dateOnlyKey, dayKeyAsUtcMidnight } from "@/lib/tz/date-only"; import { daysBetweenDateKeys, shiftDateKey, userDayKey } from "@/lib/tz/format"; +import { visitPath } from "@/lib/encounters/visit-links"; /** Everything a section reader needs about the day. */ export interface DayFrame { @@ -630,7 +631,7 @@ async function visitsPart(frame: DayFrame): Promise { meta: row.status === "PLANNED" ? "PLANNED" : row.kind, note: openText(row.outcomeEncrypted, "visit outcome"), docs: docs.get(row.id) ?? [], - href: `/checkups?visit=${row.id}`, + href: visitPath(row.id), }), ), }; diff --git a/src/lib/day/visit-preparation.ts b/src/lib/day/visit-preparation.ts index b4f6f67e2..7040dc585 100644 --- a/src/lib/day/visit-preparation.ts +++ b/src/lib/day/visit-preparation.ts @@ -26,6 +26,7 @@ import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; import { dateOnlyKey, dayKeyAsUtcMidnight } from "@/lib/tz/date-only"; import { daysBetweenDateKeys, shiftDateKey, userDayKey } from "@/lib/tz/format"; import { startOfLocalDayKey } from "@/lib/tz/local-day"; +import { visitPath } from "@/lib/encounters/visit-links"; /** The window when the record holds no visit the caller may see. */ export const FALLBACK_WINDOW_DAYS = 90; @@ -275,7 +276,7 @@ async function procedureChanges(w: Window): Promise { kind: "procedure", id: row.id, title: openText(row.reasonEncrypted, "procedure reason") ?? "", - href: `/checkups?visit=${row.id}`, + href: visitPath(row.id), }), ); } diff --git a/src/lib/encounters/__tests__/visit-links.test.ts b/src/lib/encounters/__tests__/visit-links.test.ts new file mode 100644 index 000000000..7ff94fee9 --- /dev/null +++ b/src/lib/encounters/__tests__/visit-links.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "vitest"; + +import { visitPath } from "../visit-links"; + +describe("visit links", () => { + it("opens one visit on the visits tab, never the bare checkups page", () => { + expect(visitPath("v1")).toBe("/checkups?visit=v1"); + expect(visitPath("a b&c")).toBe("/checkups?visit=a%20b%26c"); + }); +}); diff --git a/src/lib/encounters/visit-links.ts b/src/lib/encounters/visit-links.ts new file mode 100644 index 000000000..dd228a1fa --- /dev/null +++ b/src/lib/encounters/visit-links.ts @@ -0,0 +1,17 @@ +/** + * Where a visit lives in the app, spelled once. + * + * Visits have no page of their own: they are the second tab of `/checkups`, + * beside the preventive-care list that opens by default. The checkups page + * opens the visit a `visit` parameter names on the visits tab + * (`src/hooks/use-deep-linked-record.ts`). + * + * Every link to one visit is built here. + * + * Pure and client-safe: no server imports. + */ + +/** One visit: the visits tab, scrolled to the visit and opened. */ +export function visitPath(id: string): string { + return `/checkups?visit=${encodeURIComponent(id)}`; +} diff --git a/src/lib/links/related.ts b/src/lib/links/related.ts index 24bd01927..c1fc30bc1 100644 --- a/src/lib/links/related.ts +++ b/src/lib/links/related.ts @@ -34,6 +34,7 @@ import type { RELATED_KINDS, RELATED_VIA, } from "@/lib/validations/relation-links"; +import { visitPath } from "@/lib/encounters/visit-links"; export type RelatedKind = (typeof RELATED_KINDS)[number]; export type RelatedVia = (typeof RELATED_VIA)[number]; @@ -191,7 +192,7 @@ async function hydrate( row.practitioner?.name ?? row.kind, date: iso(row.occurredAt), - href: `/checkups?visit=${encodeURIComponent(row.id)}`, + href: visitPath(row.id), }); } return out; diff --git a/src/lib/mcp/record-search.ts b/src/lib/mcp/record-search.ts index 6edfa7102..34aafb724 100644 --- a/src/lib/mcp/record-search.ts +++ b/src/lib/mcp/record-search.ts @@ -73,6 +73,7 @@ import { queryWords, type SearchCandidate, } from "@/lib/search/match"; +import { visitPath } from "@/lib/encounters/visit-links"; // ── Bounds ─────────────────────────────────────────────────────────── @@ -227,7 +228,7 @@ export function recordUrl( const rid = encodeURIComponent(id); switch (kind) { case "visit": - return `${origin}/checkups?visit=${rid}`; + return `${origin}${visitPath(id)}`; case "condition": return `${origin}/illness/${rid}`; case "document": diff --git a/src/lib/timeline/lanes.ts b/src/lib/timeline/lanes.ts index f35f91489..73fdb5789 100644 --- a/src/lib/timeline/lanes.ts +++ b/src/lib/timeline/lanes.ts @@ -34,6 +34,7 @@ import { surfaceModule } from "@/lib/modules/surface"; import type { ShareDomain } from "@/lib/sharing/scope"; import { dateOnlyKey } from "@/lib/tz/date-only"; import { userDayKey } from "@/lib/tz/format"; +import { visitPath } from "@/lib/encounters/visit-links"; /** * The sharing domain each lane reads, or `null` for a lane only the record's @@ -423,7 +424,7 @@ async function visitsLane(frame: LaneFrame): Promise { r.practitioner?.name ?? "", sub: r.kind, - href: `/checkups?visit=${r.id}`, + href: visitPath(r.id), }), ), standing: [],