From 90c7da2a5eb37216d7abdeae44716c6d40c57b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 16 Jun 2026 08:36:08 +0200 Subject: [PATCH] feat(doctor-report): add workouts + lab trends + self-context, align adherence rounding Surface actual workout sessions and a per-analyte lab trend in the doctor report PDF + FHIR bundle (gated on their modules), carry the user's stored conditions/allergies through to the report and FHIR Condition/AllergyIntolerance, and round medication adherence to the app's integer convention so the PDF, FHIR and in-app numbers agree. --- src/lib/__tests__/doctor-report-data.test.ts | Bin 15931 -> 16814 bytes src/lib/doctor-report-data.ts | 23 +++++++++++++++++++ src/lib/doctor-report-pdf-core.ts | 23 +++++++++++++------ src/lib/fhir/__tests__/build-bundle.test.ts | 5 ++-- src/lib/fhir/resources.ts | 9 ++++++-- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/lib/__tests__/doctor-report-data.test.ts b/src/lib/__tests__/doctor-report-data.test.ts index 85b19408593ad3b8becd82a3ca63a7b79642bb7f..fd08a5d0ea335b8b1610c1946d85f6ac03c67dba 100644 GIT binary patch delta 814 zcmb7C&1%$86h@FP#HAHoC?6x}4a9UZLrkFPqM*A<@eM9F$4O{~m zg19NTaO=jG@JT$G$qXVR*3BX0{Ct1+>zhySubl7S(s@Citf3d0>4>}`%i%Sk)1%$b zNB4Hjwt-Oy72xp`oXKR#YUEvO^9JmoElSHYBaeD@4Mx{B<6<6m#%H9*YepT$>BzJqG=Eb89?U9 zu-Zmj#Yq|>&c;Cyu{$7@rNRDkX>V_Vr>EL53SGU#Ag8TSmB%Z1A^M=fq2?l5b}s=I zVzLHG#skn_*)1%qB*dfjMmF70!?U)kun~l^frhC_tQJErK}1ZCH=1Q-dHJo>Aw5fT zO1wB{h$Kasfc&v|RY+ldEY-E_TPq3o!H{()rPb_w_wMS=`x|DzJs}lO{s8SRZrr)M zy4b(CUWIi-S`uRJ8tShtwq6qjYXQ&24D~NJ#mwpduf!o9UXO) => string; @@ -333,10 +336,11 @@ function buildClinicalSummaryLines( const compEntries = Object.values(data.compliance); const totalDoses = compEntries.reduce((s, c) => s + c.total, 0); const totalTaken = compEntries.reduce((s, c) => s + c.taken, 0); - if (totalDoses > 0) { + const headlineRate = adherenceRatePercent(totalTaken, totalDoses); + if (headlineRate !== null) { lines.push( t("doctorReport.summaryAdherence", { - rate: num((totalTaken / totalDoses) * 100, 1), + rate: num(headlineRate, 0), }), ); } @@ -986,7 +990,9 @@ export function buildDoctorReportPdfDocument( // detail-page adherence %. The column is labelled "Expected" rather // than "Total" so the row stays internally coherent: Taken + Missed = // Expected, with Skipped shown alongside as informational. - const rate = c.total > 0 ? `${num((c.taken / c.total) * 100, 1)}%` : "—"; + // Integer percent via the canonical rounding — matches the in-app card. + const ratePct = adherenceRatePercent(c.taken, c.total); + const rate = ratePct !== null ? `${num(ratePct, 0)}%` : "—"; return [ name, String(c.taken), @@ -1090,13 +1096,16 @@ export function buildDoctorReportPdfDocument( ); y += 5; } - if (med.compliance.total > 0) { - const rate = (med.compliance.taken / med.compliance.total) * 100; + const glp1Rate = adherenceRatePercent( + med.compliance.taken, + med.compliance.total, + ); + if (glp1Rate !== null) { doc.text( t("doctorReport.glp1Compliance", { taken: med.compliance.taken, total: med.compliance.total, - rate: num(rate, 1), + rate: num(glp1Rate, 0), }), margin, y, diff --git a/src/lib/fhir/__tests__/build-bundle.test.ts b/src/lib/fhir/__tests__/build-bundle.test.ts index efa480f9e..3f95a9576 100644 --- a/src/lib/fhir/__tests__/build-bundle.test.ts +++ b/src/lib/fhir/__tests__/build-bundle.test.ts @@ -412,8 +412,9 @@ describe("buildFhirDocumentBundle", () => { const adherence = observations.find((o) => o.code.coding?.some((c) => c.code === "71799-1"), ); - // PDF renders taken/total = 85/90 = 94.4%. - expect(adherence?.valueQuantity?.value).toBeCloseTo(94.4, 1); + // taken/total = 85/90 = 94.4% → rounded to the app's integer convention + // (PDF + FHIR now share one rounding source of truth). + expect(adherence?.valueQuantity?.value).toBe(94); }); it("emits one MedicationStatement per active medication", () => { diff --git a/src/lib/fhir/resources.ts b/src/lib/fhir/resources.ts index 93de33db6..61cf6c327 100644 --- a/src/lib/fhir/resources.ts +++ b/src/lib/fhir/resources.ts @@ -17,7 +17,10 @@ * convention. All text is escaped plain text; never user-supplied HTML * (no markdown library, no `dangerouslySetInnerHTML`). */ -import type { DoctorReportData } from "@/lib/doctor-report-data"; +import { + adherenceRatePercent, + type DoctorReportData, +} from "@/lib/doctor-report-data"; import { resolveGlucoseUnit, convertGlucose } from "@/lib/glucose"; import { LOINC_SYSTEM, @@ -660,7 +663,9 @@ export function observationsFromReportData( for (const [name, comp] of Object.entries(data.compliance)) { if (comp.total <= 0) continue; obsSeq += 1; - const rate = Math.round((comp.taken / comp.total) * 1000) / 10; + // Integer percent — the one canonical rounding the app card + PDF use, so + // a clinician sees the same adherence figure on every surface. + const rate = adherenceRatePercent(comp.taken, comp.total) ?? 0; push({ resourceType: "Observation", id: `obs-${obsSeq}`,