feat(vaccinations): link documents to doses, with the upload suggestion

- from the record: a document link picker on the capture form, gated on the
  documents module (blanked when off, never post-filtered)
- from the upload: a VACCINATION scan offers to file against a dose recorded
  within the shared ±7-day window; a single candidate pre-selects, two offer a
  picker with nothing pre-selected (WR-8 seen red on the pre-select rule,
  reverted by content hash), zero shows nothing, and it never blocks the upload
- the suggestion rule reuses the visit moment's window constant rather than
  forking it; it registers with the shared filing module when that lands
- suggestion and link copy in six locales
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 21:11:21 +02:00
1 parent b5ce44628a
commit 9b58394724
15 files changed
+551 -6

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
/**
* `GET /api/vaccinations/suggest?anchor=<iso>` — which dose does a document
* dated `anchor` most plausibly belong to?
*
* The document upload review asks this when a scan is classified `VACCINATION`.
* The rule — the ±7-day window (shared with the visit moment), and the
* one-pre-selects / two-offer-a-picker verdict — lives in
* `src/lib/vaccinations/document-suggestion.ts` and is answered here so the
* browser never re-derives it.
*
* A read of the record's own doses, so it declares the same `profile` domain
* the immunization list does. `userId` is narrowed from auth; the anchor is the
* only input and it is a date, not an id.
*/
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
import { vaccinationSuggestQuerySchema } from "@/lib/validations/vaccinations";
import { suggestVaccinationForDate } from "@/lib/vaccinations/document-suggestion";
export const GET = apiHandler(async (request: NextRequest) => {
const { user } = await requireRecordAuth("read", "profile");
const params = new URL(request.url).searchParams;
const parsed = vaccinationSuggestQuerySchema.safeParse({
anchor: params.get("anchor") ?? undefined,
});
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "vaccination.invalid",
});
}
const result = await suggestVaccinationForDate(prisma, {
userId: user.id,
anchor: new Date(parsed.data.anchor),
});
annotate({
action: { name: "vaccination.dose.suggest", entity_type: "vaccination" },
meta: {
verdict: result.kind,
candidates:
result.kind === "many"
? result.vaccinations.length
: result.kind === "one"
? 1
: 0,
},
});
return apiSuccess(result);
});