fix(doctor-report): footer states the user's own timezone, not Berlin

The clinician PDF footer read "Timezone: Europe/Berlin" for every user
regardless of their actual timezone — a maintainer-anchored default leaking
into a document handed to a clinician, who then reads every timestamp on
the page against the wrong basis for a non-Berlin operator.

`userTz` already carries the resolved per-user zone into the formatters;
thread the same resolution (valid IANA name, else the Berlin fallback) into
the footer string as {timezone} instead of the literal text.
This commit is contained in:
Marc-André Bombeck committed 2026-07-17 23:12:12 +02:00
1 parent 0518c39050
commit 8c0b200a5b
7 files changed
+20 -7

No files matched your search

+1 -1
View File
@@ -91,7 +91,7 @@
"colShare": "Anteil",
"footerDisclaimer1": "Dieser Bericht wurde automatisch aus selbst erfassten Daten generiert und dient ausschließlich zur Unterstützung",
"footerDisclaimer2": "des Arzt-Patienten-Gesprächs. Er ersetzt keine ärztliche Diagnose. Korrelationen implizieren keine Kausalität.",
"footerSource": "Quelle: HealthLog | Zeitzone: Europe/Berlin | Erstellt: {timestamp}",
"footerSource": "Quelle: HealthLog | Zeitzone: {timezone} | Erstellt: {timestamp}",
"typeWeight": "Körpergewicht",
"typeBpSys": "Systolischer Blutdruck",
"typeBpDia": "Diastolischer Blutdruck",
+1 -1
View File
@@ -91,7 +91,7 @@
"colShare": "Share",
"footerDisclaimer1": "This report was generated automatically from self-tracked data and is intended to support",
"footerDisclaimer2": "the doctor–patient conversation. It does not replace a medical diagnosis. Correlations do not imply causation.",
"footerSource": "Source: HealthLog | Timezone: Europe/Berlin | Created: {timestamp}",
"footerSource": "Source: HealthLog | Timezone: {timezone} | Created: {timestamp}",
"typeWeight": "Body weight",
"typeBpSys": "Systolic blood pressure",
"typeBpDia": "Diastolic blood pressure",
+1 -1
View File
@@ -91,7 +91,7 @@
"colShare": "Porcentaje",
"footerDisclaimer1": "Este informe se generó automáticamente a partir de datos autorregistrados y está destinado a apoyar",
"footerDisclaimer2": "la conversación entre médico y paciente. No sustituye un diagnóstico médico. Las correlaciones no implican causalidad.",
"footerSource": "Fuente: HealthLog | Zona horaria: Europe/Berlin | Creado: {timestamp}",
"footerSource": "Fuente: HealthLog | Zona horaria: {timezone} | Creado: {timestamp}",
"typeWeight": "Peso corporal",
"typeBpSys": "Tensión arterial sistólica",
"typeBpDia": "Tensión arterial diastólica",
+1 -1
View File
@@ -91,7 +91,7 @@
"colShare": "Part",
"footerDisclaimer1": "Ce rapport a été généré automatiquement à partir de données auto-déclarées et vise à soutenir",
"footerDisclaimer2": "la conversation médecin-patient. Il ne remplace pas un diagnostic médical. Les corrélations n'impliquent pas la causalité.",
"footerSource": "Source : HealthLog | Fuseau horaire : Europe/Berlin | Créé : {timestamp}",
"footerSource": "Source : HealthLog | Fuseau horaire : {timezone} | Créé : {timestamp}",
"typeWeight": "Poids corporel",
"typeBpSys": "Tension artérielle systolique",
"typeBpDia": "Tension artérielle diastolique",
+1 -1
View File
@@ -91,7 +91,7 @@
"colShare": "Quota",
"footerDisclaimer1": "Questo rapporto è stato generato automaticamente da dati auto-registrati ed è destinato a supportare",
"footerDisclaimer2": "il dialogo medico-paziente. Non sostituisce una diagnosi medica. Le correlazioni non implicano causalità.",
"footerSource": "Fonte: HealthLog | Fuso orario: Europe/Berlin | Creato: {timestamp}",
"footerSource": "Fonte: HealthLog | Fuso orario: {timezone} | Creato: {timestamp}",
"typeWeight": "Peso corporeo",
"typeBpSys": "Pressione arteriosa sistolica",
"typeBpDia": "Pressione arteriosa diastolica",
+1 -1
View File
@@ -91,7 +91,7 @@
"colShare": "Udział",
"footerDisclaimer1": "Ten raport został wygenerowany automatycznie z danych samodzielnie rejestrowanych i ma na celu wspierać",
"footerDisclaimer2": "rozmowę lekarza z pacjentem. Nie zastępuje diagnozy medycznej. Korelacje nie oznaczają związku przyczynowego.",
"footerSource": "Źródło: HealthLog | Strefa czasowa: Europe/Berlin | Utworzono: {timestamp}",
"footerSource": "Źródło: HealthLog | Strefa czasowa: {timezone} | Utworzono: {timestamp}",
"typeWeight": "Masa ciała",
"typeBpSys": "Ciśnienie skurczowe",
"typeBpDia": "Ciśnienie rozkurczowe",
+14 -1
View File
@@ -12,9 +12,14 @@
import { jsPDF } from "jspdf";
import autoTable from "jspdf-autotable";
import { DOCTOR_REPORT_TYPE_LABEL_KEYS } from "./doctor-report/type-label-keys";
import { makeFormatters, type TimeFormatPreference } from "./format-locale";
import {
makeFormatters,
DISPLAY_TIMEZONE,
type TimeFormatPreference,
} from "./format-locale";
import type { Locale } from "./i18n/config";
import { convertGlucose, resolveGlucoseUnit } from "./glucose";
import { isValidTimezone } from "./tz/format";
import {
classifyReferenceRange,
formatReferenceRange,
@@ -505,6 +510,13 @@ export function buildDoctorReportPdfDocument(
const num = (value: number, decimals = 1) =>
formatters.number(value, decimals);
const fmtDate = (iso: string) => formatters.date(iso);
// Same resolution `makeFormatters` applies internally (valid IANA name or
// the Europe/Berlin fallback) — the footer must state the SAME zone the
// timestamps above it were actually rendered in, not a maintainer-anchored
// default. A clinician reading a non-Berlin operator's PDF needs the real
// provenance of every timestamp on the page.
const footerTz =
userTz && isValidTimezone(userTz) ? userTz : DISPLAY_TIMEZONE;
const unitFor = (type: string): string => {
const staticUnit = DOCTOR_REPORT_TYPE_UNIT_KEYS[type];
@@ -1797,6 +1809,7 @@ export function buildDoctorReportPdfDocument(
pageDoc.text(t("doctorReport.footerDisclaimer2"), margin, pageHeight - 10);
pageDoc.text(
t("doctorReport.footerSource", {
timezone: footerTz,
timestamp: formatters.dateTime(now),
}),
margin,