fix(doctor-report): label the adherence denominator column as Expected

The dose-ledger adherence rate divides taken by taken + missed (deliberate
skips excluded), so the PDF compliance table's denominator column no longer
equals Taken + Skipped + Missed. Relabel it "Expected" so each row stays
internally coherent — Taken + Missed = Expected, with Skipped shown alongside
as informational and the rate reading Taken / Expected.

Also tighten the coach adherence-block wording: the headline is the
dose-weighted pool of taken / (taken + missed) across the user's scheduled
medications — exactly one medication's card rate for a single-med user, and the
honest overall adherence for several — never a per-day or per-week denominator
of its own.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 13:27:21 +02:00
1 parent 8e50a9b4cd
commit 461ed5b5e0
8 files changed
+28 -12

No files matched your search

+1
View File
@@ -75,6 +75,7 @@
"colSkipped": "Übersprungen",
"colMissed": "Verpasst",
"colTotal": "Gesamt",
"colExpected": "Erwartet",
"colComplianceRate": "Therapietreue",
"moodTitle": "Stimmungsverlauf",
"moodSummary": "Durchschnitt: {avg} / 5,0 · Einträge: {count} · Bereich: {min}–{max}",
+1
View File
@@ -75,6 +75,7 @@
"colSkipped": "Skipped",
"colMissed": "Missed",
"colTotal": "Total",
"colExpected": "Expected",
"colComplianceRate": "Adherence rate",
"moodTitle": "Mood trajectory",
"moodSummary": "Average: {avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+1
View File
@@ -75,6 +75,7 @@
"colSkipped": "Omitida",
"colMissed": "Perdida",
"colTotal": "Total",
"colExpected": "Esperadas",
"colComplianceRate": "Tasa de cumplimiento",
"moodTitle": "Evolución del estado de ánimo",
"moodSummary": "Promedio:{avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+1
View File
@@ -75,6 +75,7 @@
"colSkipped": "Sauté",
"colMissed": "Manqué",
"colTotal": "Total",
"colExpected": "Attendues",
"colComplianceRate": "Taux d'observance",
"moodTitle": "Évolution de l’humeur",
"moodSummary": "Moyenne:{avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+1
View File
@@ -75,6 +75,7 @@
"colSkipped": "Saltata",
"colMissed": "Persa",
"colTotal": "Totale",
"colExpected": "Attese",
"colComplianceRate": "Tasso di aderenza",
"moodTitle": "Andamento dell’umore",
"moodSummary": "Media:{avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+1
View File
@@ -75,6 +75,7 @@
"colSkipped": "Pominięte",
"colMissed": "Pominięte",
"colTotal": "Razem",
"colExpected": "Oczekiwane",
"colComplianceRate": "Wskaźnik przestrzegania",
"moodTitle": "Przebieg nastroju",
"moodSummary": "Średnia:{avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+16 -11
View File
@@ -994,13 +994,15 @@ async function buildCoachSnapshotImpl(
// denominator, and cross-source duplicate rows collapse onto one slot.
const ledgerRows: DoseHistoryRow[] = [];
// v1.17 W1c — the coach's headline adherence figure routes through the
// SAME `calculateCompliance(...).rate` authority the medication card
// shows (the ledger path, `medicationContext` supplied), so the coach
// can never quote a denominator the card doesn't use. We tally
// numerator (on-time + late takes) and denominator (taken + missed)
// across every compliance medication over the coach window, exactly the
// ledger arithmetic the card's per-med rate uses; the per-day / per-week
// timeline below stays as supplementary context.
// SAME `calculateCompliance(...).rate` ledger authority the medication
// card shows (the ledger path, `medicationContext` supplied), so the
// coach can never quote a denominator the card doesn't use. Per
// medication we take the ledger numerator (on-time + late takes) and
// denominator (taken + missed) and pool them across the user's
// scheduled medications: for a single medication the headline equals
// that med's card rate exactly; for several it is the dose-weighted
// overall adherence (the same pooling the cross-med timeline below
// already uses), never a per-day / per-week denominator of its own.
let complianceTaken = 0;
let complianceDenominator = 0;
const windowDaysForRate = Math.max(
@@ -1092,10 +1094,13 @@ async function buildCoachSnapshotImpl(
.sort((a, b) => a.weekISO.localeCompare(b.weekISO));
snapshot.compliance = {
// v1.17 W1c — headline adherence % from the SAME ledger authority
// (`calculateCompliance(...).rate`) the medication card shows, so the
// coach quotes the exact figure on the card rather than a per-day /
// per-week rate built off a different denominator. Integer 0-100 to
// match the card's rounding.
// (`calculateCompliance(...).rate`) the medication card shows: the
// dose-weighted pool of taken / (taken + missed) across the user's
// scheduled medications, so the coach quotes the card's figure
// (single med) or its honest overall adherence (several meds) rather
// than a per-day / per-week rate built off a different denominator.
// Integer 0-100 to match the card's rounding; null when no scheduled
// medication has any countable dose in the window.
rate:
complianceDenominator > 0
? Math.round((complianceTaken / complianceDenominator) * 100)
+6 -1
View File
@@ -870,6 +870,11 @@ export function buildDoctorReportPdfDocument(
y += 6;
const compRows = complianceEntries.map(([name, c]) => {
// v1.17 W1a — `total` is the ledger rate denominator (taken + missed,
// deliberate skips excluded), so `taken / total` matches the app's
// 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)}%` : "—";
return [
name,
@@ -889,7 +894,7 @@ export function buildDoctorReportPdfDocument(
t("doctorReport.colTaken"),
t("doctorReport.colSkipped"),
t("doctorReport.colMissed"),
t("doctorReport.colTotal"),
t("doctorReport.colExpected"),
t("doctorReport.colComplianceRate"),
],
],