From cfe5e2d3c64b3d73b04e3610f5ee66970003f791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 21 Oct 2026 23:31:58 +0200 Subject: [PATCH] fix(report): release the medication effect only with the medication list The effect block names every medication it describes and its confounders, so a report or share link that withheld the medication list still carried the names. The aggregator now loads the block only when both leaves are admitted, the clinician view checks the list leaf as well, and the leaf label says it needs the list. --- messages/de.json | 2 +- messages/en.json | 2 +- messages/es.json | 2 +- messages/fr.json | 2 +- messages/it.json | 2 +- messages/ko.json | 2 +- messages/nl.json | 2 +- messages/pl.json | 2 +- .../__tests__/clinician-view.test.tsx | 47 +++++++++++++++++++ .../clinician/activity-effect-sections.tsx | 6 ++- src/lib/doctor-report-types.ts | 5 +- .../__tests__/leaf-gating-sweep.test.ts | 27 +++++++++++ src/lib/doctor-report/collect.ts | 6 ++- 13 files changed, 95 insertions(+), 12 deletions(-) diff --git a/messages/de.json b/messages/de.json index f3e2a8458..332273ff2 100644 --- a/messages/de.json +++ b/messages/de.json @@ -12427,7 +12427,7 @@ "leafSurgicalHistory": "Operationen und Eingriffe", "leafImmunizations": "Impfungen", "leafEmergency": "Notfalldaten (Blutgruppe, Kontakte, Verfügungen)", - "leafMedicationEffect": "Veränderung rund um den Start jedes Medikaments", + "leafMedicationEffect": "Veränderung rund um den Start jedes Medikaments (braucht die Medikamentenliste)", "leafWorkouts": "Trainings (Übersicht je Sportart)", "leafHrvSdnn": "HRV (SDNN)" }, diff --git a/messages/en.json b/messages/en.json index 292300819..df76fca81 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12427,7 +12427,7 @@ "leafSurgicalHistory": "Surgical history", "leafImmunizations": "Immunizations", "leafEmergency": "Emergency information (blood type, contacts, directives)", - "leafMedicationEffect": "Change around each medication start", + "leafMedicationEffect": "Change around each medication start (needs the medication list)", "leafWorkouts": "Workouts (summary per sport)", "leafHrvSdnn": "HRV (SDNN)" }, diff --git a/messages/es.json b/messages/es.json index 2e222ca60..a41820b06 100644 --- a/messages/es.json +++ b/messages/es.json @@ -12427,7 +12427,7 @@ "leafSurgicalHistory": "Antecedentes quirúrgicos", "leafImmunizations": "Vacunas", "leafEmergency": "Información de emergencia (grupo sanguíneo, contactos, voluntades)", - "leafMedicationEffect": "Cambio en torno al inicio de cada medicamento", + "leafMedicationEffect": "Cambio en torno al inicio de cada medicamento (requiere la lista de medicamentos)", "leafWorkouts": "Entrenamientos (resumen por deporte)", "leafHrvSdnn": "VFC (SDNN)" }, diff --git a/messages/fr.json b/messages/fr.json index ce2b8f4ff..2c7feebcd 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -12427,7 +12427,7 @@ "leafSurgicalHistory": "Antécédents chirurgicaux", "leafImmunizations": "Vaccinations", "leafEmergency": "Informations d'urgence (groupe sanguin, contacts, directives)", - "leafMedicationEffect": "Changement autour du démarrage de chaque médicament", + "leafMedicationEffect": "Changement autour du démarrage de chaque médicament (nécessite la liste des médicaments)", "leafWorkouts": "Entraînements (résumé par sport)", "leafHrvSdnn": "VFC (SDNN)" }, diff --git a/messages/it.json b/messages/it.json index f7a72c126..7d623bd08 100644 --- a/messages/it.json +++ b/messages/it.json @@ -12427,7 +12427,7 @@ "leafSurgicalHistory": "Anamnesi chirurgica", "leafImmunizations": "Vaccinazioni", "leafEmergency": "Informazioni di emergenza (gruppo sanguigno, contatti, disposizioni)", - "leafMedicationEffect": "Variazione intorno all'inizio di ogni farmaco", + "leafMedicationEffect": "Variazione intorno all'inizio di ogni farmaco (richiede l'elenco dei farmaci)", "leafWorkouts": "Allenamenti (riepilogo per sport)", "leafHrvSdnn": "HRV (SDNN)" }, diff --git a/messages/ko.json b/messages/ko.json index dce35a661..bbfc55376 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -12427,7 +12427,7 @@ "leafSurgicalHistory": "수술 이력", "leafImmunizations": "예방접종", "leafEmergency": "응급 정보 (혈액형, 연락처, 사전지시)", - "leafMedicationEffect": "각 약 시작 전후의 변화", + "leafMedicationEffect": "각 약 시작 전후의 변화 (약 목록 필요)", "leafWorkouts": "운동(종목별 요약)", "leafHrvSdnn": "심박변이도 (SDNN)" }, diff --git a/messages/nl.json b/messages/nl.json index 1af10b087..3b16274f8 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -12427,7 +12427,7 @@ "leafSurgicalHistory": "Operatiegeschiedenis", "leafImmunizations": "Vaccinaties", "leafEmergency": "Noodinformatie (bloedgroep, contactpersonen, wilsverklaringen)", - "leafMedicationEffect": "Verandering rond de start van elk medicijn", + "leafMedicationEffect": "Verandering rond de start van elk medicijn (vereist de medicatielijst)", "leafWorkouts": "Trainingen (overzicht per sport)", "leafHrvSdnn": "HRV (SDNN)" }, diff --git a/messages/pl.json b/messages/pl.json index ccbf60069..c1e0aee67 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -12427,7 +12427,7 @@ "leafSurgicalHistory": "Historia operacji", "leafImmunizations": "Szczepienia", "leafEmergency": "Informacje na wypadek nagły (grupa krwi, kontakty, oświadczenia)", - "leafMedicationEffect": "Zmiana wokół rozpoczęcia każdego leku", + "leafMedicationEffect": "Zmiana wokół rozpoczęcia każdego leku (wymaga listy leków)", "leafWorkouts": "Treningi (podsumowanie według dyscypliny)", "leafHrvSdnn": "HRV (SDNN)" }, diff --git a/src/components/clinician/__tests__/clinician-view.test.tsx b/src/components/clinician/__tests__/clinician-view.test.tsx index 2492591bc..7a765a28e 100644 --- a/src/components/clinician/__tests__/clinician-view.test.tsx +++ b/src/components/clinician/__tests__/clinician-view.test.tsx @@ -163,6 +163,53 @@ describe("", () => { expect(html).not.toContain("Current smoker"); }); + it("shows the medication effect only beside the medication list", () => { + const report = makeReport({ + medicationEffects: [ + { + medicationName: "Named effect drug", + startsOn: "2026-01-05", + targets: [ + { + type: "BLOOD_PRESSURE_SYS", + effect: { + present: false, + reason: "insufficient_after", + level: 0.95, + window: null, + days: { + before: 40, + after: 3, + excludedLowAdherence: 0, + excludedNotTaking: 0, + }, + adherence: { + status: "tracked", + floorPct: 80, + lookbackDays: 14, + }, + levelChange: null, + slopeChangePerWeek: null, + preSlopePerWeek: null, + fit: null, + confounders: [], + }, + }, + ], + }, + ], + }); + const leaves = ["MEDICATION_EFFECT", "BLOOD_PRESSURE_SYS"] as const; + expect( + render(report, { + selection: selectionFromLeaves([...leaves, "MEDICATION_LIST"]), + }), + ).toContain("Named effect drug"); + expect( + render(report, { selection: selectionFromLeaves([...leaves]) }), + ).not.toContain("Named effect drug"); + }); + it("names both windows when a lab's report and the saved band disagree", () => { // Two windows that disagree about the same number, and a card showing one // of them is a partial answer. The PDF settles this with a footnote under diff --git a/src/components/clinician/activity-effect-sections.tsx b/src/components/clinician/activity-effect-sections.tsx index a920eb3c7..33f07921a 100644 --- a/src/components/clinician/activity-effect-sections.tsx +++ b/src/components/clinician/activity-effect-sections.tsx @@ -113,7 +113,11 @@ export function MedicationEffectSection({ /** A stated calendar day (YYYY-MM-DD) in the owner's date order. */ fmtDay: (day: string) => string; }) { - const effects = report.medicationEffects ?? []; + // The block names medications: without the list leaf it shows nothing, + // whatever the payload carries. + const effects = scope.admits("MEDICATION_LIST") + ? (report.medicationEffects ?? []) + : []; const glucoseUnit = resolveGlucoseUnit(report.glucoseUnit ?? null); return ( { expect(off.medications.length).toBeGreaterThan(0); }); + it("withholds the medication effect without the medication list", async () => { + // The effect block names every medication it describes, so withholding + // the list must withhold the names here too. + const leaves: ReportLeafId[] = [ + "MEDICATION_EFFECT", + ...MEASUREMENT_LEAF_IDS, + ]; + const withList = await collectDoctorReportData( + "u1", + RANGE, + selectionFromLeaves([...leaves, "MEDICATION_LIST"]), + { moduleMap: moduleMap() }, + ); + expect(withList.medicationEffects?.length ?? 0).toBeGreaterThan(0); + + const withoutList = await collectDoctorReportData( + "u1", + RANGE, + selectionFromLeaves(leaves), + { moduleMap: moduleMap() }, + ); + expect(withoutList.medicationEffects ?? null).toBeNull(); + expect(JSON.stringify(withoutList)).not.toContain( + withList.medicationEffects![0].medicationName, + ); + }); + it("withholds the glucose panel while the raw series stays", async () => { const withPanel = await collectDoctorReportData( "u1", diff --git a/src/lib/doctor-report/collect.ts b/src/lib/doctor-report/collect.ts index a38c3313b..371d49918 100644 --- a/src/lib/doctor-report/collect.ts +++ b/src/lib/doctor-report/collect.ts @@ -674,7 +674,11 @@ export async function collectDoctorReportData( : Promise.resolve(null), // The effect around each listed medication's start. Its own leaf AND the // target's measurement leaf must both admit it; the loader asks the gate. - gate.admits("MEDICATION_EFFECT") + // The block names every medication it describes (and the confounders), + // so it also needs the medication list: an owner who withheld the list + // never releases the names through the effect block. Same rule as the + // clinician view's compliance rows, which are named from the list. + gate.admits("MEDICATION_EFFECT") && gate.admits("MEDICATION_LIST") ? loadReportMedicationEffects({ userId, medicationIds: medications.map((m) => m.id),