diff --git a/src/__tests__/doctor-report-control-gating-guard.test.ts b/src/__tests__/doctor-report-control-gating-guard.test.ts index ad5fd641d..36a3a0765 100644 --- a/src/__tests__/doctor-report-control-gating-guard.test.ts +++ b/src/__tests__/doctor-report-control-gating-guard.test.ts @@ -74,9 +74,9 @@ describe("report scope gating — structural guard", () => { it("reads a plausible catalogue", () => { // Sanity floor: a silently degraded catalogue would pass everything below // vacuously. - expect(ALL_LEAF_IDS.length).toBe(94); + expect(ALL_LEAF_IDS.length).toBe(95); expect(Object.keys(MEASUREMENT_LEAF_GROUP)).toHaveLength(77); - expect(Object.keys(STRUCTURED_LEAF_GROUP)).toHaveLength(17); + expect(Object.keys(STRUCTURED_LEAF_GROUP)).toHaveLength(18); }); it("places every leaf in exactly one group", () => { diff --git a/src/__tests__/share-view-leaf-render-guard.test.ts b/src/__tests__/share-view-leaf-render-guard.test.ts index 1d42a23a7..5f073d4f5 100644 --- a/src/__tests__/share-view-leaf-render-guard.test.ts +++ b/src/__tests__/share-view-leaf-render-guard.test.ts @@ -13,7 +13,7 @@ * is `STRUCTURED_LEAF_GROUP`, the other is HTML rendered by the real * `` over a real fixture. It proves four things per leaf. * - * 1. `DISPOSITIONS` is `Record`, so an eighteenth + * 1. `DISPOSITIONS` is `Record`, so a nineteenth * structured leaf cannot be added to the catalogue without deciding here * whether it renders — and saying so — or `pnpm typecheck` fails. That is * the lock, and it is the compiler's, not this suite's. @@ -332,6 +332,22 @@ const DISPOSITIONS: Record = { }, marker: "Palpitations", }, + SURGICAL_HISTORY: { + kind: "rendered", + data: { + surgicalHistory: [ + { + occurredAt: "2011-03-14T09:00:00.000Z", + procedure: "Meniscus repair", + bodySite: "Knee", + laterality: "LEFT", + outcome: null, + practitionerName: null, + }, + ], + }, + marker: "Meniscus repair", + }, IMMUNIZATIONS: { kind: "rendered", data: { @@ -419,7 +435,7 @@ describe("clinician view — every catalogue leaf has a renderer or a reason", ( it("reads a plausible catalogue", () => { // Sanity floor: a degraded import would satisfy every loop below // vacuously, which is the failure mode a pair guard dies of. - expect(STRUCTURED_LEAF_IDS).toHaveLength(17); + expect(STRUCTURED_LEAF_IDS).toHaveLength(18); expect(Object.keys(DISPOSITIONS).sort()).toEqual( [...STRUCTURED_LEAF_IDS].sort(), ); diff --git a/src/components/clinician/clinician-view.tsx b/src/components/clinician/clinician-view.tsx index 12e45cc89..fb49c7b2e 100644 --- a/src/components/clinician/clinician-view.tsx +++ b/src/components/clinician/clinician-view.tsx @@ -46,6 +46,7 @@ import { EmergencySection, PatientIdentitySection } from "./identity-sections"; import { IllnessSection, ImmunizationsSection, + SurgicalHistorySection, LabResultsSection, VisitsSection, } from "./history-sections"; @@ -256,6 +257,12 @@ export function ClinicianView({ scope={scope} fmtDate={fmtDate} /> + string; +}) { + const procedures = report.surgicalHistory ?? []; + + const site = (row: (typeof procedures)[number]) => { + if (!row.bodySite) return null; + if (!row.laterality) return row.bodySite; + return t("encounters.bodySiteWithSide", { + site: row.bodySite, + side: t(lateralityLabelKey(row.laterality as Laterality)), + }); + }; + + return ( + + {procedures.map((row, index) => ( + + ))} + + ); +} + /** * The immunization record. Reference data, not window-bounded: an Impfpass is * a lifetime document and the whole of it rides when the leaf and the module diff --git a/src/components/settings/__tests__/report-selection-panel.test.tsx b/src/components/settings/__tests__/report-selection-panel.test.tsx index d18a73f5c..394ee3b19 100644 --- a/src/components/settings/__tests__/report-selection-panel.test.tsx +++ b/src/components/settings/__tests__/report-selection-panel.test.tsx @@ -192,8 +192,8 @@ describe(" — the first run", () => { expect(groupChip(html, "body")).toBe("0/12"); expect(groupChip(html, "labs")).toBe("0/1"); expect(groupChip(html, "medications")).toBe("0/4"); - // ALLERGIES, ILLNESS_EPISODES and now VISITS. - expect(groupChip(html, "history")).toBe("0/4"); + // ALLERGIES, ILLNESS_EPISODES, VISITS, SURGICAL_HISTORY, IMMUNIZATIONS. + expect(groupChip(html, "history")).toBe("0/5"); expect(groupChip(html, "cardio")).toBe("0/14"); expect(groupChip(html, "activity")).toBe("0/8"); expect(groupChip(html, "sleepRecovery")).toBe("0/16"); diff --git a/src/lib/__tests__/doctor-report-pdf-core.test.ts b/src/lib/__tests__/doctor-report-pdf-core.test.ts index 0c95abaf3..c3b7e3b8b 100644 --- a/src/lib/__tests__/doctor-report-pdf-core.test.ts +++ b/src/lib/__tests__/doctor-report-pdf-core.test.ts @@ -641,6 +641,82 @@ describe("doctor-report illness section", () => { }); }); +// v1.39.1 — the surgical history. Lifetime reference data like the +// immunization record: every procedure that happened, with its date, what was +// done (the visit's reason), where, and what came of it. Present only when the +// SURGICAL_HISTORY leaf was admitted and at least one procedure exists. +// +// Mutation checks (each run, each seen red): +// - drop the section's `if` block from the renderer → "prints the surgical +// history table" goes red; +// - print `bodySite` without the side → "names the side beside the site" +// goes red. +describe("doctor-report surgical history section", () => { + const PROCEDURES: NonNullable = [ + { + occurredAt: "2011-03-14T09:00:00.000Z", + procedure: "Meniscus repair", + bodySite: "Knee", + laterality: "LEFT", + outcome: "Full recovery after physiotherapy", + practitionerName: "Sample clinic", + }, + { + occurredAt: "2019-07-02T09:00:00.000Z", + procedure: null, + bodySite: null, + laterality: null, + outcome: null, + practitionerName: null, + }, + ]; + + it("prints the surgical history table with procedure, site and outcome", async () => { + const bytes = renderDoctorReportPdfBytes( + makeData({ surgicalHistory: PROCEDURES }), + { + timeFormat: "AUTO", + dateFormat: "AUTO", + t: getServerTranslator("en").t, + locale: "en", + now: FIXED_NOW, + }, + ); + const text = await extractText(bytes); + expect(text).toContain("Surgical history"); + expect(text).toContain("Meniscus repair"); + expect(text).toContain("Full recovery after physiotherapy"); + }); + + it("names the side beside the site", async () => { + const bytes = renderDoctorReportPdfBytes( + makeData({ surgicalHistory: PROCEDURES }), + { + timeFormat: "AUTO", + dateFormat: "AUTO", + t: getServerTranslator("de").t, + locale: "de", + now: FIXED_NOW, + }, + ); + const text = await extractText(bytes); + expect(text).toContain("Operationen und Eingriffe"); + expect(text).toMatch(/Knee · Links/); + }); + + it("omits the section when there is no procedure", async () => { + const bytes = renderDoctorReportPdfBytes(makeData(), { + timeFormat: "AUTO", + dateFormat: "AUTO", + t: getServerTranslator("en").t, + locale: "en", + now: FIXED_NOW, + }); + const text = await extractText(bytes); + expect(text).not.toContain("Surgical history"); + }); +}); + // ── emergency first page ── the Notfalldaten sheet owns page one, but only // when the aggregator populated `data.emergency` (the EMERGENCY leaf admitted // AND the profile holds something). A null payload — which is exactly what the diff --git a/src/lib/doctor-report-pdf/clinical-records-notes-section.ts b/src/lib/doctor-report-pdf/clinical-records-notes-section.ts index 6c1087ead..c01a1397b 100644 --- a/src/lib/doctor-report-pdf/clinical-records-notes-section.ts +++ b/src/lib/doctor-report-pdf/clinical-records-notes-section.ts @@ -1,7 +1,10 @@ import type { jsPDF } from "jspdf"; import autoTable from "jspdf-autotable"; -import type { EncounterKind } from "@/generated/prisma/client"; -import { encounterKindLabelKey } from "../encounters/kind-label"; +import type { EncounterKind, Laterality } from "@/generated/prisma/client"; +import { + encounterKindLabelKey, + lateralityLabelKey, +} from "../encounters/kind-label"; import { classifyReferenceRange, formatReferenceRange, @@ -289,6 +292,72 @@ export function buildClinicalRecordsNotesSection( .finalY + 8; } + // The surgical history. Reference data (not time-windowed), populated when + // the SURGICAL_HISTORY leaf is selected AND a procedure happened. Oldest + // first, the order an intake form asks for it in. What was done is the + // visit's reason; the site carries its side after it in the report's + // language. Same calm register as the tables around it. + if (data.surgicalHistory && data.surgicalHistory.length > 0) { + y = ensureSpace(y, 6 + 18); + doc.setFontSize(14); + doc.setFont("helvetica", "bold"); + doc.setTextColor(30, 30, 30); + doc.text(t("doctorReport.surgicalHistoryTitle"), margin, y); + y += 6; + + const siteCell = (site: string | null, side: string | null): string => { + if (!site) return "—"; + if (!side) return site; + return t("encounters.bodySiteWithSide", { + site, + side: t(lateralityLabelKey(side as Laterality)), + }); + }; + + const procedureRows = data.surgicalHistory.map((row) => [ + fmtDate(row.occurredAt), + row.procedure ?? "—", + siteCell(row.bodySite, row.laterality), + row.outcome ?? "—", + ]); + + autoTable(doc, { + startY: y, + head: [ + [ + t("doctorReport.surgicalHistoryColDate"), + t("doctorReport.surgicalHistoryColProcedure"), + t("doctorReport.surgicalHistoryColBodySite"), + t("doctorReport.surgicalHistoryColOutcome"), + ], + ], + body: procedureRows, + theme: "grid", + styles: { + fontSize: 9, + cellPadding: 3, + textColor: [30, 30, 30], + lineColor: [200, 200, 200], + lineWidth: 0.3, + }, + headStyles: { + fillColor: [245, 245, 245], + textColor: [30, 30, 30], + fontStyle: "bold", + }, + alternateRowStyles: { fillColor: [252, 252, 252] }, + margin: { + left: margin, + right: margin, + top: margin, + bottom: tableBottomMargin, + }, + }); + y = + (doc as jsPDF & { lastAutoTable: { finalY: number } }).lastAutoTable + .finalY + 8; + } + // The immunization history. Reference data (not time-windowed), populated // when the `IMMUNIZATIONS` leaf is selected AND the `vaccinations` module is // on AND live doses exist. A compact chronological table — date, vaccine diff --git a/src/lib/doctor-report-types.ts b/src/lib/doctor-report-types.ts index 34cb5605a..df46db213 100644 --- a/src/lib/doctor-report-types.ts +++ b/src/lib/doctor-report-types.ts @@ -308,6 +308,27 @@ export interface DoctorReportData { /** Labels of the conditions this visit was filed against. */ conditionLabels: string[]; }> | null; + /** + * The surgical history (v1.39.1): every visit of kind PROCEDURE that + * happened, oldest first. Reference data, not time-windowed, like the + * immunization record below — an operation fifteen years ago is still the + * answer to "what surgeries have you had". Null when the SURGICAL_HISTORY + * leaf was not selected or no procedure exists; both builders then skip the + * section. Optional so older fixtures still typecheck. + * + * `procedure` is the visit's reason, which is where a person writes what was + * done. `procedure`, `bodySite` and `outcome` are decrypted free text and + * null on a key-rotation gap. `laterality` rides as the enum constant; the + * renderer names it in the report's language. + */ + surgicalHistory?: Array<{ + occurredAt: string; + procedure: string | null; + bodySite: string | null; + laterality: string | null; + outcome: string | null; + practitionerName: string | null; + }> | null; /** * The immunization history. Reference data, not time-windowed — an Impfpass * is a lifetime document and a doctor wants the whole of it, so this mirrors diff --git a/src/lib/doctor-report/__tests__/leaf-gating-sweep.test.ts b/src/lib/doctor-report/__tests__/leaf-gating-sweep.test.ts index 5ebd93d96..7517335c4 100644 --- a/src/lib/doctor-report/__tests__/leaf-gating-sweep.test.ts +++ b/src/lib/doctor-report/__tests__/leaf-gating-sweep.test.ts @@ -299,6 +299,7 @@ describe("per-leaf gating sweep", () => { expect(allPayload.familyHistory).not.toBeNull(); expect(allPayload.illnessEpisodes).not.toBeNull(); expect(allPayload.visits).not.toBeNull(); + expect(allPayload.surgicalHistory).not.toBeNull(); expect(allPayload.mood).not.toBeNull(); expect(allPayload.cycle).not.toBeNull(); expect(allPayload.anamnesis).not.toBeNull(); @@ -454,18 +455,23 @@ describe("zero read for unchosen leaves", () => { ["LAB_RESULTS", "labResult"], ["ILLNESS_EPISODES", "illnessEpisode"], ["VISITS", "encounter"], + ["SURGICAL_HISTORY", "encounter"], ["IMMUNIZATIONS", "vaccinationRecord"], ["ANAMNESIS", "userHealthProfile"], ["EMERGENCY", "userHealthProfile"], ["ANAMNESIS", "healthProfileFactRevision"], ]; - // ANAMNESIS and EMERGENCY both read `userHealthProfile`, so withholding one + // ANAMNESIS and EMERGENCY both read `userHealthProfile` (and VISITS and + // SURGICAL_HISTORY both read `encounter`), so withholding one // while the other stays selected still queries the table. Both must be off // before the read stops — the zero-read guarantee is over the SET of leaves // that touch a table, not each one in isolation. const TABLE_READERS: Record = { userHealthProfile: ["ANAMNESIS", "EMERGENCY"], + // The visits in the window and the lifetime surgical history both read + // the encounter table, under their own leaves. + encounter: ["VISITS", "SURGICAL_HISTORY"], }; it.each(gated)( diff --git a/src/lib/doctor-report/__tests__/surgical-history.test.ts b/src/lib/doctor-report/__tests__/surgical-history.test.ts new file mode 100644 index 000000000..cd94f2f7d --- /dev/null +++ b/src/lib/doctor-report/__tests__/surgical-history.test.ts @@ -0,0 +1,95 @@ +/** + * The surgical history's read for the doctor report. + * + * Lifetime reference data, like the immunization record: a knee operated on + * fifteen years ago is still the answer to "what surgeries have you had", so + * the read ignores the report window. It reads only procedures that happened, + * and the free text (what was done, where, what came of it) decrypts fail-soft + * per row, so one key-rotation gap does not blank the section. + * + * Mutation checks (each run, each seen red): + * - add the report window to the `where` → "ignores the report window" goes + * red; + * - drop `status: "DONE"` → "reads only procedures that happened" goes red; + * - let a decrypt error throw → "reads a row it cannot decrypt as a blank" + * goes red with the thrown error. + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +process.env.ENCRYPTION_KEY ??= + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +const findMany = vi.hoisted(() => vi.fn()); +vi.mock("@/lib/db", () => ({ prisma: { encounter: { findMany } } })); +vi.mock("@/lib/logging/context", () => ({ getEvent: () => undefined })); + +import { encryptToBytes } from "@/lib/ai/coach/bytes-codec"; +import { loadSurgicalHistory } from "../clinical-records"; + +beforeEach(() => { + findMany.mockReset(); +}); + +describe("loadSurgicalHistory", () => { + it("reads only procedures that happened, over the whole record", async () => { + findMany.mockResolvedValue([]); + await loadSurgicalHistory("u1"); + const args = findMany.mock.calls[0][0]; + expect(args.where).toEqual({ + userId: "u1", + deletedAt: null, + kind: "PROCEDURE", + status: "DONE", + }); + expect(args.orderBy).toEqual({ occurredAt: "asc" }); + }); + + it("ignores the report window", async () => { + findMany.mockResolvedValue([]); + await loadSurgicalHistory("u1"); + expect(findMany.mock.calls[0][0].where).not.toHaveProperty("occurredAt"); + }); + + it("returns null when there is no procedure", async () => { + findMany.mockResolvedValue([]); + expect(await loadSurgicalHistory("u1")).toBeNull(); + }); + + it("maps a procedure to date, what, where and outcome", async () => { + findMany.mockResolvedValue([ + { + occurredAt: new Date("2011-03-14T09:00:00.000Z"), + reasonEncrypted: encryptToBytes("Meniscus repair"), + outcomeEncrypted: encryptToBytes("Full recovery"), + bodySiteEncrypted: encryptToBytes("Knee"), + laterality: "LEFT", + practitioner: { name: "Sample clinic" }, + }, + ]); + expect(await loadSurgicalHistory("u1")).toEqual([ + { + occurredAt: "2011-03-14T09:00:00.000Z", + procedure: "Meniscus repair", + bodySite: "Knee", + laterality: "LEFT", + outcome: "Full recovery", + practitionerName: "Sample clinic", + }, + ]); + }); + + it("reads a row it cannot decrypt as a blank rather than failing the report", async () => { + findMany.mockResolvedValue([ + { + occurredAt: new Date("2019-07-02T09:00:00.000Z"), + reasonEncrypted: new Uint8Array([1, 2, 3, 4]), + outcomeEncrypted: null, + bodySiteEncrypted: null, + laterality: null, + practitioner: null, + }, + ]); + const rows = await loadSurgicalHistory("u1"); + expect(rows?.[0].procedure).toBeNull(); + }); +}); diff --git a/src/lib/doctor-report/clinical-records.ts b/src/lib/doctor-report/clinical-records.ts index 84ba348bd..5383d61de 100644 --- a/src/lib/doctor-report/clinical-records.ts +++ b/src/lib/doctor-report/clinical-records.ts @@ -199,6 +199,58 @@ export async function loadVisits( return mapped.length > 0 ? mapped : null; } +/** + * The surgical history: every procedure that happened, oldest first. + * + * Reference data, not windowed — like the immunization history below, it is a + * lifetime answer, and a report over the last 90 days that left out a knee + * operation from 2011 would answer "what surgeries have you had" wrongly. + * Planned, cancelled and missed procedures are not surgical history. The three + * free-text columns decrypt fail-soft per row, the same stance `loadVisits` + * takes. + */ +export async function loadSurgicalHistory( + userId: string, +): Promise { + const rows = await prisma.encounter.findMany({ + where: { userId, deletedAt: null, kind: "PROCEDURE", status: "DONE" }, + orderBy: { occurredAt: "asc" }, + select: { + occurredAt: true, + reasonEncrypted: true, + outcomeEncrypted: true, + bodySiteEncrypted: true, + laterality: true, + practitioner: { select: { name: true } }, + }, + }); + if (rows.length === 0) return null; + + const decrypt = ( + value: Uint8Array | null, + field: "reason" | "outcome" | "bodySite", + ): string | null => { + if (!value || value.byteLength === 0) return null; + try { + return decryptFromBytes(value); + } catch { + getEvent()?.addWarning( + `doctor-report: procedure ${field} decrypt failed for ${userId}`, + ); + return null; + } + }; + + return rows.map((row) => ({ + occurredAt: row.occurredAt.toISOString(), + procedure: decrypt(row.reasonEncrypted, "reason"), + bodySite: decrypt(row.bodySiteEncrypted, "bodySite"), + laterality: row.laterality, + outcome: decrypt(row.outcomeEncrypted, "outcome"), + practitionerName: row.practitioner?.name ?? null, + })); +} + /** * The immunization history. Reference data, not time-windowed — an Impfpass is * a lifetime document, so like allergies it ignores the report window. diff --git a/src/lib/doctor-report/collect.ts b/src/lib/doctor-report/collect.ts index d6bccfced..1f52651b1 100644 --- a/src/lib/doctor-report/collect.ts +++ b/src/lib/doctor-report/collect.ts @@ -70,6 +70,7 @@ import { loadIllnessEpisodes, loadImmunizations, loadVisits, + loadSurgicalHistory, loadLabResults, } from "./clinical-records"; @@ -508,6 +509,7 @@ export async function collectDoctorReportData( labResults, illnessEpisodes, visits, + surgicalHistory, immunizations, allergies, familyHistory, @@ -526,6 +528,10 @@ export async function collectDoctorReportData( gate.admits("VISITS") ? loadVisits(userId, start, end) : Promise.resolve(null), + // Reference data, not windowed, like the immunization history below. + gate.admits("SURGICAL_HISTORY") + ? loadSurgicalHistory(userId) + : Promise.resolve(null), // Reference data, not windowed — the immunization history is a lifetime // document, so the whole live set rides when the leaf and module admit it. gate.admits("IMMUNIZATIONS") @@ -601,6 +607,7 @@ export async function collectDoctorReportData( labResults, illnessEpisodes, visits, + surgicalHistory, immunizations, allergies, familyHistory, diff --git a/src/lib/fhir/build-bundle.ts b/src/lib/fhir/build-bundle.ts index 6947a0f8e..8c1fc2edb 100644 --- a/src/lib/fhir/build-bundle.ts +++ b/src/lib/fhir/build-bundle.ts @@ -277,6 +277,13 @@ export function buildFhirDocumentBundle( // both carry the doses; the Bundle does not, and this comment is where a // reader who notices should land. // + // The surgical history (v1.39.1) rides the same decision. A procedure is a + // visit of kind PROCEDURE, and FHIR's `Procedure` resource would map it + // cleanly (`code.text` from the reason, `bodySite.text` from the site), but + // emitting one without its REST route and capability entry is the same + // half-shipped state. The PDF and the clinician view carry the section; the + // Bundle does not. + // // held an episode (the aggregator gates `data.illnessEpisodes`). Each // episode emits a patient-reported Condition (generic SNOMED root, label on // `code.text`) plus a bounding Encounter that references it. diff --git a/src/lib/report-selection/__tests__/catalogue-guard.test.ts b/src/lib/report-selection/__tests__/catalogue-guard.test.ts index 038c50b57..952114f1f 100644 --- a/src/lib/report-selection/__tests__/catalogue-guard.test.ts +++ b/src/lib/report-selection/__tests__/catalogue-guard.test.ts @@ -52,8 +52,8 @@ function resolves(key: string): boolean { describe("report selection catalogue", () => { it("carries every measurement type and every structured leaf", () => { expect(MEASUREMENT_LEAF_IDS).toHaveLength(77); - expect(STRUCTURED_LEAF_IDS).toHaveLength(17); - expect(ALL_LEAF_IDS).toHaveLength(94); + expect(STRUCTURED_LEAF_IDS).toHaveLength(18); + expect(ALL_LEAF_IDS).toHaveLength(95); }); it("resolves a label for every leaf", () => { diff --git a/src/lib/report-selection/catalogue.ts b/src/lib/report-selection/catalogue.ts index 681112289..b0c71e16d 100644 --- a/src/lib/report-selection/catalogue.ts +++ b/src/lib/report-selection/catalogue.ts @@ -48,6 +48,7 @@ export type StructuredLeafId = | "ALLERGIES" | "ILLNESS_EPISODES" | "VISITS" + | "SURGICAL_HISTORY" | "IMMUNIZATIONS" | "FAMILY_HISTORY" | "MOOD" @@ -204,6 +205,7 @@ export const STRUCTURED_LEAF_GROUP: Record = { ALLERGIES: "history", ILLNESS_EPISODES: "history", VISITS: "history", + SURGICAL_HISTORY: "history", IMMUNIZATIONS: "history", FAMILY_HISTORY: "sensitive", MOOD: "sensitive", @@ -211,7 +213,7 @@ export const STRUCTURED_LEAF_GROUP: Record = { ANAMNESIS: "sensitive", }; -/** i18n label keys for the 17 structured leaves. */ +/** i18n label keys for the 18 structured leaves. */ export const STRUCTURED_LEAF_LABEL_KEYS: Record = { PATIENT_IDENTITY: "reportSelection.leafPatientIdentity", EMERGENCY: "reportSelection.leafEmergency", @@ -225,6 +227,7 @@ export const STRUCTURED_LEAF_LABEL_KEYS: Record = { ALLERGIES: "reportSelection.leafAllergies", ILLNESS_EPISODES: "reportSelection.leafIllnessEpisodes", VISITS: "reportSelection.leafVisits", + SURGICAL_HISTORY: "reportSelection.leafSurgicalHistory", IMMUNIZATIONS: "reportSelection.leafImmunizations", FAMILY_HISTORY: "reportSelection.leafFamilyHistory", MOOD: "reportSelection.leafMood", @@ -323,6 +326,7 @@ export const LEAF_MODULE: Partial> = { // no module of its own — it is core, like the checkups page it lives on — // and the whole report is already gated by `doctorReport`. Reading the // absence as an oversight is the mistake this comment exists to prevent. + // SURGICAL_HISTORY is absent for the same reason: a procedure is a visit. }; /** Every measurement leaf id, in enum order. */ diff --git a/src/lib/report-selection/template.ts b/src/lib/report-selection/template.ts index c5246cc08..01e2a04a3 100644 --- a/src/lib/report-selection/template.ts +++ b/src/lib/report-selection/template.ts @@ -16,7 +16,8 @@ import { type ReportLeafId } from "./catalogue"; /** * Identity and insurer (the cover page a practice files the document under), * the three classic vitals, weight and BMI, the glucose series and its panel, - * labs, the drug list and adherence, allergies and past illness episodes. + * labs, the drug list and adherence, allergies, past illness episodes, the + * surgical history and the immunization record. * * Deliberately not in it: everything a wearable produces, gait and mobility, * environment exposure, the per-dose administration ledger (a clinician asks @@ -39,6 +40,7 @@ export const STANDARD_TEMPLATE_LEAVES: readonly ReportLeafId[] = [ "MEDICATION_COMPLIANCE", "ALLERGIES", "ILLNESS_EPISODES", + "SURGICAL_HISTORY", "IMMUNIZATIONS", ];