feat(documents): link a document to visits and vaccinations from its own sheet

A document could show its visits read-only and its vaccinations not at
all, so a childhood record covering a dozen doses had to be attached
from a dozen dose forms. The document's detail sheet now lists the
visits and vaccinations it is filed against and, for someone who may
manage the vault, links and unlinks them in one place, with the records
dated near the document on top. Both ends write the same link tables
through the link service, so the dose's form and the document's sheet
cannot disagree about what is filed where.

The document detail response carries the linked doses, named by
catalogue slug or the person's own wording. It is null rather than
empty for a grant that opens the vault but not the health background,
so a shared page never claims to record no doses. The update route
takes a replace-set of vaccination ids, up to 100 so one record can
cover a whole primary schedule; ownership is re-checked, a foreign id
answers 404 and the change lands in the audit log beside the other link
fields. Linking from the dose suggestion now refreshes the sheet.

Refs #1024, #1005
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 09:39:27 +02:00
1 parent f46ed99def
commit b480ac4543
23 files changed
+1012 -87

No files matched your search

+1
View File
@@ -128,6 +128,7 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
"documents.inbound.restoreGone",
"documents.inbound.uploadBusy",
"documents.inbound.uploadTimeout",
"documents.inbound.vaccinationNotFound",
],
encounter: [
"encounter.invalid",
+18 -1
View File
@@ -98,7 +98,7 @@ const inboundFactEditRequest = inboundFactEditSchema.meta({
const documentUpdateRequest = documentUpdateSchema.meta({
id: "DocumentUpdateRequest",
description:
"Metadata edit for a stored document: `title` (user label; null clears it), `kind` (category), `documentDate` (user filing date, YYYY-MM-DD; null clears it), `episodeIds` (REPLACE-SET of condition links — the document's links become exactly this set; an empty array unlinks everything; every id must be a live episode of the caller or the whole request answers 404), `encounterIds` (the same replace-set semantics over the caller's visits). At least one field required. No `userId` field — narrowed from the session and fed to the Prisma `where` with the row id.",
"Metadata edit for a stored document: `title` (user label; null clears it), `kind` (category), `documentDate` (user filing date, YYYY-MM-DD; null clears it), `episodeIds` (REPLACE-SET of condition links — the document's links become exactly this set; an empty array unlinks everything; every id must be a live episode of the caller or the whole request answers 404), `encounterIds` (the same replace-set semantics over the caller's visits), `vaccinationIds` (the same replace-set semantics over the caller's vaccination doses, up to 100 so one childhood record can be filed against a whole primary schedule). At least one field required. No `userId` field — narrowed from the session and fed to the Prisma `where` with the row id.",
});
const documentBulkRequest = documentBulkSchema.meta({
@@ -156,6 +156,19 @@ const encounterLink = z
"A link to one of the caller's visits — \"this letter belongs to that appointment\". `kind` is the visit-kind ENUM CONSTANT rather than a rendered name: unlike a condition label, which is the person's own words, a visit kind is one of eight closed values whose human name the reader's own bundle owns, so publishing the constant is what lets a German reader and an English one each see it named correctly.",
});
const vaccinationLink = z
.object({
vaccinationId: z.string(),
occurredAt: z.string().nullable(),
catalogSlug: z.string().nullable(),
vaccineName: z.string().nullable(),
})
.meta({
id: "DocumentVaccinationLink",
description:
"A link to one of the caller's vaccination doses — \"this page records that dose\". `catalogSlug` is the catalogue entry when this release still knows the dose's slug, for the reader's own bundle to name; null is the signal to show `vaccineName`, the person's own wording. The same table the dose's own `/api/vaccinations/{id}/links` writes from the other end.",
});
const inboundDocument = z
.object({
id: z.string(),
@@ -203,6 +216,10 @@ const inboundDocument = z
const inboundDocumentDetail = inboundDocument
.extend({
facts: z.array(extractedFact),
vaccinationLinks: z.array(vaccinationLink).nullable().meta({
description:
"Vaccination doses this document is filed against. Null when the caller acts inside somebody else's record under a grant that does not cover the health background the doses live in: the page is shared, the doses are not, and an empty list would claim the page records none.",
}),
summary: z.string().nullable(),
summaryGeneratedAt: z.string().nullable(),
summaryState: z.enum(DOCUMENT_SUMMARY_STATES),