fix(visits): reach every lab result, condition, document and visit from the link pickers

The visit form's lab picker offered the newest 200 results, the condition
picker the newest 100 conditions, the document pickers the newest thousand
documents and the document sheet's visit picker the newest 100 visits.
On a long record everything older could not be linked.

Each picker now opens on the records around the date of the thing being
linked, pages older and newer from there, and searches through the list
route itself. Records already linked always show, from the labels the
record's own read carries and from what was picked in the sheet.

The lab list takes a q filter over analyte and panel, the condition list
from/to/q/offset/sortDir, and the visit list an offset.
This commit is contained in:
Marc-André Bombeck committed 2026-10-15 11:24:40 +02:00
1 parent dad9a7778e
commit 73c4dda224
35 files changed
+1714 -251

No files matched your search

@@ -1,11 +1,11 @@
/**
* One key, one fetch. The visit form's link pickers read labs and illness
* episodes with their own page size (200 results, 100 episodes), the Labs
* list and the illness hook with theirs (500, the route's default 50). Under
* a shared key whichever read landed first served the other, so the picker
* could offer 50 episodes or the Labs list could render a 200-row page as
* the whole list. The picker reads carry their own keys, still under the
* domain roots so a lab or episode write evicts them.
* episodes in their own windows and pages, the Labs list and the illness hook
* with theirs. Under a shared key whichever read landed first served the
* other, so the picker could offer 50 episodes or the Labs list could render
* a picker page as the whole list. The picker reads carry their own keys,
* still under the domain roots so a lab or episode write evicts them, and
* each direction, split and term is its own key.
*/
import { describe, expect, it } from "vitest";
@@ -22,20 +22,36 @@ const LIST_PARAMS = {
describe("the link pickers' reads have keys of their own", () => {
it("labs: differs from the Labs list and sits under the lab root", () => {
const key = queryKeys.labResultsPicker(200);
const key = queryKeys.labResultsPicker("older", null, "");
expect(key).not.toEqual(queryKeys.labResultsList(LIST_PARAMS));
expect(key.slice(0, 1)).toEqual(queryKeys.labResults());
});
it("episodes: differs from the illness list and sits under the illness root", () => {
const key = queryKeys.illnessEpisodesPicker(100);
const key = queryKeys.illnessEpisodesPicker("older", null, "");
expect(key).not.toEqual(queryKeys.illnessEpisodes(true));
expect(key.slice(0, 1)).toEqual(queryKeys.illness());
});
it("a different page size is a different key", () => {
expect(queryKeys.labResultsPicker(200)).not.toEqual(
queryKeys.labResultsPicker(500),
it("a different direction, split or term is a different key", () => {
const base = queryKeys.labResultsPicker("older", "2019-05-17", "");
expect(base).not.toEqual(
queryKeys.labResultsPicker("newer", "2019-05-17", ""),
);
expect(base).not.toEqual(
queryKeys.labResultsPicker("older", "2020-01-01", ""),
);
expect(queryKeys.labResultsPicker("search", null, "a")).not.toEqual(
queryKeys.labResultsPicker("search", null, "b"),
);
});
it("documents and visits: under their roots", () => {
expect(
queryKeys.inboundDocumentVaultPicker("older", null, "").slice(0, 1),
).toEqual(["documents"]);
expect(queryKeys.encounterPicker("older", null, "").slice(0, 1)).toEqual(
queryKeys.encounters(),
);
});
});
+17 -5
View File
@@ -61,12 +61,24 @@ export const documentKeys = {
inboundDocumentPicker: (q: string) =>
["documents", "inbound", "picker", q] as const,
/**
* The whole vault as a link-picker option list (every page, walked at the
* list route's ceiling), shared by the visit and vaccination forms. Own
* segment because the shape is a flat document array, not a page.
* The vault as a link-picker option list, shared by the visit and
* vaccination forms: one paged direction from the record's date or a
* search. Own segment because the page shape is the picker's, not the
* library's.
*/
inboundDocumentVaultPicker: () =>
["documents", "inbound", "vault-picker"] as const,
inboundDocumentVaultPicker: (
direction: string,
pivot: string | null,
term: string,
) =>
[
"documents",
"inbound",
"vault-picker",
direction,
pivot ?? "",
term,
] as const,
/**
* Storage usage + effective limits (`GET /api/documents/inbound/usage`).
* Invalidated through the `["documents"]` prefix after every upload /
+7
View File
@@ -45,6 +45,13 @@ export const encounterKeys = {
status ?? null,
episodeId ?? null,
] as const,
/**
* The document sheet's visit picker: one paged direction from the
* document's date. Its own key because its page shape (offset steps inside
* a window) is not the list's.
*/
encounterPicker: (direction: string, pivot: string | null, term: string) =>
["encounters", "picker", direction, pivot ?? "", term] as const,
encounter: (id: string) => ["encounters", "detail", id] as const,
/**
* The procedure history for one filter. Under the `["encounters"]` root, so
+8 -4
View File
@@ -15,10 +15,14 @@ export const illnessKeys = {
illness: () => ["illness"] as const,
illnessEpisodes: (includeResolved: boolean) =>
["illness", "episodes", includeResolved] as const,
// The visit form's episode picker, with its own page size (the illness
// pages read the route's default); a shared key would serve one to the other.
illnessEpisodesPicker: (limit: number) =>
["illness", "episodes-picker", limit] as const,
// The visit form's episode picker: one paged direction from the visit's
// date or a search, with its own page shape; a shared key would serve one
// read to the other.
illnessEpisodesPicker: (
direction: string,
pivot: string | null,
term: string,
) => ["illness", "episodes-picker", direction, pivot ?? "", term] as const,
illnessEpisode: (id: string) => ["illness", "episode", id] as const,
illnessDayLog: (episodeId: string, date: string) =>
["illness", "day-log", episodeId, date] as const,
+6 -5
View File
@@ -36,11 +36,12 @@ export const labKeys = {
labAnalyteLink: (analyte: string) =>
["lab-results", "analyte-link", analyte] as const,
// The visit form's lab picker: newest results, its own page size. Its own
// key because the Labs list reads a different page size, and a shared key
// would serve one read's page to the other.
labResultsPicker: (limit: number) =>
["lab-results", "picker", limit] as const,
// The visit form's lab picker: one paged direction from the visit's date
// (`older` / `newer`) or a search across the whole history. Its own key
// because the Labs list reads other pages, and a shared key would serve
// one read's page to the other.
labResultsPicker: (direction: string, pivot: string | null, term: string) =>
["lab-results", "picker", direction, pivot ?? "", term] as const,
// v1.25 — paginated (offset) reading feed for the biomarker detail page.
// Distinct from `labResultsList` (single-page key) so the infinite-query
+5
View File
@@ -164,6 +164,11 @@ export const encounterListQuerySchema = z
*/
episodeId: id.optional(),
limit: z.coerce.number().int().min(1).max(200).optional(),
/**
* Rows to skip in each arm, for a picker that pages a long history in
* `limit`-sized steps inside a date window.
*/
offset: z.coerce.number().int().min(0).max(100_000).optional(),
})
.strict()
.refine((q) => !(q.from && q.to) || q.from <= q.to, {
+11
View File
@@ -136,6 +136,17 @@ export type IllnessEpisodeResolve = z.infer<typeof illnessEpisodeResolveSchema>;
export const illnessEpisodeListQuerySchema = z.object({
limit: z.coerce.number().int().min(1).max(100).optional(),
includeResolved: z.union([z.literal("true"), z.literal("false")]).optional(),
/**
* Paging and a date window over the onset, for a picker that opens on the
* episodes around a visit and pages older or newer from there.
*/
offset: z.coerce.number().int().min(0).max(100_000).optional(),
from: z.iso.datetime({ offset: true }).optional(),
to: z.iso.datetime({ offset: true }).optional(),
/** Onset order; newest first by default. */
sortDir: z.enum(["asc", "desc"]).optional(),
/** Case-insensitive substring over the episode's label. */
q: z.string().trim().min(1).max(120).optional(),
});
/* ── day-log capture (one row per day of an episode) ─────────────── */
+5
View File
@@ -284,6 +284,11 @@ export const listLabResultsSchema = z.object({
biomarkerId: z.string().trim().min(1).max(64).optional(),
analyte: z.string().trim().min(1).max(120).optional(),
panel: z.string().trim().min(1).max(120).optional(),
/**
* Case-insensitive substring over the analyte and the panel name, so a
* picker can find one result in a long history without paging to it.
*/
q: z.string().trim().min(1).max(120).optional(),
from: z
.string()
.datetime()