mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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:
1 parent
dad9a7778e
commit
73c4dda224
35 files changed
+1714
-251
No files matched your search
@@ -4,12 +4,26 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({ user: { modules: { vaccinations: true } } }),
|
||||
}));
|
||||
vi.mock("@/hooks/use-encounters", () => ({
|
||||
useEncounters: () => ({
|
||||
data: { upcoming: [], past: [] },
|
||||
isPending: false,
|
||||
isError: false,
|
||||
refetch: vi.fn(),
|
||||
vi.mock("@/components/links/use-link-picker-pages", async (importOriginal) => ({
|
||||
...(await importOriginal<
|
||||
typeof import("@/components/links/use-link-picker-pages")
|
||||
>()),
|
||||
// The visit picker's pages: none loaded, nothing more to page.
|
||||
useLinkPickerPages: () => ({
|
||||
items: [],
|
||||
pending: false,
|
||||
error: false,
|
||||
retry: vi.fn(),
|
||||
term: "",
|
||||
setTerm: vi.fn(),
|
||||
searchPending: false,
|
||||
filterLocally: true,
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
loadingOlder: false,
|
||||
loadingNewer: false,
|
||||
loadOlder: vi.fn(),
|
||||
loadNewer: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/components/vaccinations/use-vaccinations", () => ({
|
||||
|
||||
@@ -23,18 +23,31 @@
|
||||
* reach the doses (`vaccinationLinks: null`) shows no dose block rather than
|
||||
* an empty one that would claim the page records none.
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { CalendarDays, Syringe } from "lucide-react";
|
||||
|
||||
import { EntityLinkPicker } from "@/components/links/entity-link-picker";
|
||||
import {
|
||||
EntityLinkPicker,
|
||||
serverPagingFrom,
|
||||
} from "@/components/links/entity-link-picker";
|
||||
import type { EntityLinkOption } from "@/components/links/entity-link-picker";
|
||||
import { isNearAnchor } from "@/components/links/vault-document-options";
|
||||
import { encounterPickerUrl } from "@/components/links/picker-requests";
|
||||
import {
|
||||
PICKER_PAGE_SIZE,
|
||||
pickerPivot,
|
||||
useLinkPickerPages,
|
||||
type PickerPage,
|
||||
type PickerPageRequest,
|
||||
} from "@/components/links/use-link-picker-pages";
|
||||
import { encounterKindText } from "@/components/encounters/encounter-labels";
|
||||
import { useVaccinations } from "@/components/vaccinations/use-vaccinations";
|
||||
import { useEncounters } from "@/hooks/use-encounters";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
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";
|
||||
|
||||
@@ -75,6 +88,25 @@ export function recordOptions(
|
||||
return [...near, ...rest];
|
||||
}
|
||||
|
||||
/**
|
||||
* One picker page of visits. The route answers in two arms, each paged by the
|
||||
* same offset; another page exists while either arm came back full.
|
||||
*/
|
||||
export async function fetchEncounterPage(
|
||||
request: PickerPageRequest,
|
||||
): Promise<PickerPage<EncounterDTO>> {
|
||||
const list = await apiGet<EncounterListDTO>(encounterPickerUrl(request));
|
||||
const full =
|
||||
list.upcoming.length >= PICKER_PAGE_SIZE ||
|
||||
list.past.length >= PICKER_PAGE_SIZE;
|
||||
return {
|
||||
items: [...list.upcoming, ...list.past],
|
||||
next: full
|
||||
? String(Number(request.cursor ?? "0") + PICKER_PAGE_SIZE)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Where a visit opens: its sheet on the checkups page. */
|
||||
function visitHref(encounterId: string): string {
|
||||
return `/checkups?visit=${encodeURIComponent(encounterId)}`;
|
||||
@@ -132,7 +164,16 @@ export function DocumentRecordLinks({
|
||||
const vaccinationsOn = user?.modules?.vaccinations !== false;
|
||||
const anchor = doc.documentDate ?? doc.reportDate;
|
||||
|
||||
const encounters = useEncounters(canManage);
|
||||
// The visits around the document's date, paged older and newer from
|
||||
// there. The visit route has no text search (its free text is ciphertext
|
||||
// at rest), so the term filters what is loaded.
|
||||
const encounters = useLinkPickerPages<EncounterDTO>({
|
||||
enabled: canManage,
|
||||
pivot: pickerPivot(anchor),
|
||||
queryKey: queryKeys.encounterPicker,
|
||||
fetchPage: fetchEncounterPage,
|
||||
serverSearch: false,
|
||||
});
|
||||
const vaccinations = useVaccinations(
|
||||
null,
|
||||
canManage && vaccinationsOn && doc.vaccinationLinks !== null,
|
||||
@@ -150,16 +191,16 @@ export function DocumentRecordLinks({
|
||||
};
|
||||
|
||||
const visitOptions = recordOptions(
|
||||
[
|
||||
...(encounters.data?.upcoming ?? []),
|
||||
...(encounters.data?.past ?? []),
|
||||
].map((visit) => ({
|
||||
id: visit.id,
|
||||
label: encounterKindText(t, visit.kind as EncounterKind),
|
||||
meta: visit.practitioner?.name ?? null,
|
||||
occurredAt: visit.occurredAt,
|
||||
href: visitHref(visit.id),
|
||||
})),
|
||||
[...encounters.items]
|
||||
// Two arms per page arrive in two orders; the list reads newest first.
|
||||
.sort((a, b) => b.occurredAt.localeCompare(a.occurredAt))
|
||||
.map((visit) => ({
|
||||
id: visit.id,
|
||||
label: encounterKindText(t, visit.kind as EncounterKind),
|
||||
meta: visit.practitioner?.name ?? null,
|
||||
occurredAt: visit.occurredAt,
|
||||
href: visitHref(visit.id),
|
||||
})),
|
||||
anchor,
|
||||
labels,
|
||||
);
|
||||
@@ -176,6 +217,19 @@ export function DocumentRecordLinks({
|
||||
|
||||
const showDoses = vaccinationsOn && doc.vaccinationLinks !== null;
|
||||
|
||||
// The visits the document is filed against, as its own read names them,
|
||||
// so their chips show whatever page of visits is loaded.
|
||||
const knownVisits = useMemo(
|
||||
() =>
|
||||
doc.encounterLinks.map((link) => ({
|
||||
id: link.encounterId,
|
||||
label: encounterKindText(t, link.kind as EncounterKind),
|
||||
dateLabel: link.occurredAt ? format.date(link.occurredAt) : null,
|
||||
href: visitHref(link.encounterId),
|
||||
})),
|
||||
[doc.encounterLinks, t, format],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{canManage ? (
|
||||
@@ -183,11 +237,11 @@ export function DocumentRecordLinks({
|
||||
icon={CalendarDays}
|
||||
title={t("documents.detail.visitsLabel")}
|
||||
slot="document-visit-links"
|
||||
pending={encounters.isPending || !seedFresh}
|
||||
error={encounters.isError || seedError}
|
||||
pending={encounters.pending || !seedFresh}
|
||||
error={encounters.error || seedError}
|
||||
errorLabel={t("links.picker.loadError")}
|
||||
onRetry={() => {
|
||||
if (encounters.isError) void encounters.refetch();
|
||||
if (encounters.error) encounters.retry();
|
||||
if (seedError) onRetrySeed?.();
|
||||
}}
|
||||
selected={visitIds ?? doc.encounterLinks.map((l) => l.encounterId)}
|
||||
@@ -196,6 +250,8 @@ export function DocumentRecordLinks({
|
||||
onChange({ encounterIds: ids });
|
||||
}}
|
||||
options={visitOptions}
|
||||
server={serverPagingFrom(encounters)}
|
||||
knownOptions={knownVisits}
|
||||
searchPlaceholder={t("links.picker.searchPlaceholder")}
|
||||
emptyLabel={t("documents.detail.noVisitsToLink")}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user