refactor(visits): build every link to a visit in one place

Seven surfaces spelled `/checkups?visit=<id>` by hand, some encoding
the id and some not. They now share `visitPath()` from
`src/lib/encounters/visit-links.ts`, the place the next fix extends.
No behaviour change: a cuid encodes to itself.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 12:22:22 +02:00
1 parent 421aacf457
commit 78bacb0d86
9 files changed
+41 -7

No files matched your search

@@ -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<typeof useTranslations>["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. */
+2 -1
View File
@@ -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<typeof useTranslations>["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,
{
+2 -1
View File
@@ -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<SectionPart> {
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),
}),
),
};
+2 -1
View File
@@ -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<DayChange[]> {
kind: "procedure",
id: row.id,
title: openText(row.reasonEncrypted, "procedure reason") ?? "",
href: `/checkups?visit=${row.id}`,
href: visitPath(row.id),
}),
);
}
@@ -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");
});
});
+17
View File
@@ -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)}`;
}
+2 -1
View File
@@ -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;
+2 -1
View File
@@ -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":
+2 -1
View File
@@ -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<LaneRead> {
r.practitioner?.name ??
"",
sub: r.kind,
href: `/checkups?visit=${r.id}`,
href: visitPath(r.id),
}),
),
standing: [],