diff --git a/messages/de.json b/messages/de.json index 019dcdcde..fb5a53612 100644 --- a/messages/de.json +++ b/messages/de.json @@ -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}", diff --git a/messages/en.json b/messages/en.json index cf984502e..c3c3976a4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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}", diff --git a/messages/es.json b/messages/es.json index 7d69659f0..460dae452 100644 --- a/messages/es.json +++ b/messages/es.json @@ -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}", diff --git a/messages/fr.json b/messages/fr.json index 7b2c9bd70..d280518b2 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -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}", diff --git a/messages/it.json b/messages/it.json index 91f9469e4..b9a74e8cc 100644 --- a/messages/it.json +++ b/messages/it.json @@ -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}", diff --git a/messages/pl.json b/messages/pl.json index f1f976d49..8a422b2d3 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -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}", diff --git a/src/lib/ai/coach/snapshot.ts b/src/lib/ai/coach/snapshot.ts index c594c688b..970727e6c 100644 --- a/src/lib/ai/coach/snapshot.ts +++ b/src/lib/ai/coach/snapshot.ts @@ -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) diff --git a/src/lib/doctor-report-pdf-core.ts b/src/lib/doctor-report-pdf-core.ts index 55506fef9..1a14e0543 100644 --- a/src/lib/doctor-report-pdf-core.ts +++ b/src/lib/doctor-report-pdf-core.ts @@ -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"), ], ],