feat(doctor-report): add a surgical history section

New structured leaf SURGICAL_HISTORY in the history group and in the
standard template: every procedure that took place, oldest first, with
its date, what was done (the visit's reason), the body site with its
side and the outcome. Lifetime reference data like the immunization
record, so it ignores the report window. The PDF prints it as a table
and the clinician share page renders it; saved selections do not widen.

The FHIR bundle carries no Procedure resource, on the same terms as
visits: no REST route or capability entry exists for it.

Refs #1025
This commit is contained in:
Marc-André Bombeck committed 2026-09-25 08:11:42 +02:00
1 parent ad0d50fa28
commit 0b28498d83
16 files changed
+428 -16

No files matched your search

@@ -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}
/>
<SurgicalHistorySection
t={t}
report={report}
scope={scope}
fmtDate={fmtDate}
/>
<ImmunizationsSection
t={t}
report={report}
+53 -3
View File
@@ -1,6 +1,6 @@
/**
* The `labs` and `history` groups on the clinician view: lab results, illness
* episodes, visits and the immunization record.
* episodes, visits, the surgical history and the immunization record.
*
* All four were selectable on a share link and none of them reached the page,
* while the PDF download from the same link printed every one. What each
@@ -12,8 +12,11 @@
* A pure server component: no client hooks, no session, no markdown — every
* value renders as escaped React text.
*/
import type { EncounterKind } from "@/generated/prisma/client";
import { encounterKindLabelKey } from "@/lib/encounters/kind-label";
import type { EncounterKind, Laterality } from "@/generated/prisma/client";
import {
encounterKindLabelKey,
lateralityLabelKey,
} from "@/lib/encounters/kind-label";
import { formatReferenceRange } from "@/lib/labs/reference-range";
import type { DoctorReportData } from "@/lib/doctor-report-data";
import {
@@ -221,6 +224,53 @@ export function VisitsSection({
);
}
/**
* The surgical history. Reference data, not window-bounded, like the
* immunization record below: every procedure that happened, oldest first. What
* was done is the row's label; the date, the site with its side and the
* outcome compose onto the value side, the shape the immunization rows use.
*/
export function SurgicalHistorySection({
t,
report,
scope,
fmtDate,
}: {
t: Translate;
report: DoctorReportData;
scope: LeafScope;
fmtDate: (iso: string) => 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 (
<LeafSection
t={t}
scope={scope}
leaves={["SURGICAL_HISTORY"]}
title={t("doctorReport.surgicalHistoryTitle")}
empty={procedures.length === 0}
>
{procedures.map((row, index) => (
<StatRow
key={`${row.occurredAt}-${index}`}
label={row.procedure ?? "—"}
value={compose([fmtDate(row.occurredAt), site(row), row.outcome])}
/>
))}
</LeafSection>
);
}
/**
* 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
@@ -192,8 +192,8 @@ describe("<HealthRecordExportPanel> — 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");