mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
feat(doctor-report): GLP1 therapy section with titration + weight curve + side-effects
Phase 10 of W4d GLP-1 full integration. The doctor-report PDF gains a dedicated "GLP-1 therapy" section when the patient has at least one active GLP-1 medication and the existing compliance toggle is on. The section lists what a prescribing clinician needs at every follow-up: - Weight delta over the report window (start → end → delta) - Per-medication block: drug name, current dose + start date, compliance count and rate, full titration history table (date | dose | note) - Side-effect frequency table (last N days, counts per tag) when the mood section is also enabled (mood is the data source) Generic accounts (no GLP-1 medication) get `glp1: null` from the aggregator and the PDF skips the entire section, so existing reports stay byte-identical. Aggregator extension lives in `doctor-report-data.ts` — eager-loads `doseChanges` + the most recent intake event on every active medication (cheap, indexed); GLP-1 specifics only assemble when at least one GLP-1 row exists. The field is optional on the type to preserve compatibility with existing test fixtures. DE + EN copy for the report section lives under `doctorReport.glp1*`.
This commit is contained in:
1 parent
7b6b9161c3
commit
f45adb29f0
4 files changed
+159
No files matched your search
@@ -70,6 +70,15 @@
|
||||
"bmiTitle": "Body-Mass-Index (BMI)",
|
||||
"bmiRow": "BMI: {bmi} kg/m² — Klassifikation: {class} (WHO)",
|
||||
"complianceTitle": "Medikamenten-Compliance",
|
||||
"glp1Title": "GLP-1-Therapie",
|
||||
"glp1WeightSummary": "Gewicht im Berichtszeitraum: {start} kg → {end} kg ({delta} kg)",
|
||||
"glp1CurrentDose": "Aktuelle Dosis: {value} {unit} (seit {since})",
|
||||
"glp1Compliance": "Compliance: {taken} / {total} ({rate}%)",
|
||||
"glp1SideEffectsTitle": "Erfasste Nebenwirkungen",
|
||||
"colGlp1Date": "Datum",
|
||||
"colGlp1Dose": "Dosis",
|
||||
"colGlp1Note": "Notiz",
|
||||
"colGlp1SideEffect": "Nebenwirkung",
|
||||
"colMedication": "Medikament",
|
||||
"colTaken": "Eingenommen",
|
||||
"colSkipped": "Übersprungen",
|
||||
|
||||
@@ -70,6 +70,15 @@
|
||||
"bmiTitle": "Body Mass Index (BMI)",
|
||||
"bmiRow": "BMI: {bmi} kg/m² — Classification: {class} (WHO)",
|
||||
"complianceTitle": "Medication compliance",
|
||||
"glp1Title": "GLP-1 therapy",
|
||||
"glp1WeightSummary": "Weight over report period: {start} kg → {end} kg ({delta} kg)",
|
||||
"glp1CurrentDose": "Current dose: {value} {unit} (since {since})",
|
||||
"glp1Compliance": "Compliance: {taken} / {total} ({rate}%)",
|
||||
"glp1SideEffectsTitle": "Side effects logged",
|
||||
"colGlp1Date": "Date",
|
||||
"colGlp1Dose": "Dose",
|
||||
"colGlp1Note": "Note",
|
||||
"colGlp1SideEffect": "Side effect",
|
||||
"colMedication": "Medication",
|
||||
"colTaken": "Taken",
|
||||
"colSkipped": "Skipped",
|
||||
|
||||
Binary file not shown.
@@ -455,6 +455,147 @@ export function buildDoctorReportPdfDocument(
|
||||
.finalY + 8;
|
||||
}
|
||||
|
||||
// v1.4.25 W4d — GLP-1 therapy section. Only renders when the data
|
||||
// aggregator emitted a non-null `glp1` block (user has at least one
|
||||
// active GLP-1 medication and the compliance toggle is on). Lists
|
||||
// current drug + dose, full titration history, weight curve over
|
||||
// the report window, side-effect frequency, and compliance %.
|
||||
if (data.glp1) {
|
||||
if (y > 220) {
|
||||
doc.addPage();
|
||||
y = margin;
|
||||
}
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(t("doctorReport.glp1Title"), margin, y);
|
||||
y += 6;
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.setFont("helvetica", "normal");
|
||||
if (data.glp1.weightDeltaKg !== null && data.glp1.weightStartKg !== null && data.glp1.weightEndKg !== null) {
|
||||
doc.text(
|
||||
t("doctorReport.glp1WeightSummary", {
|
||||
start: num(data.glp1.weightStartKg, 1),
|
||||
end: num(data.glp1.weightEndKg, 1),
|
||||
delta: num(data.glp1.weightDeltaKg, 1),
|
||||
}),
|
||||
margin,
|
||||
y,
|
||||
);
|
||||
y += 6;
|
||||
}
|
||||
|
||||
for (const med of data.glp1.medications) {
|
||||
if (y > 240) {
|
||||
doc.addPage();
|
||||
y = margin;
|
||||
}
|
||||
doc.setFontSize(11);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(med.name, margin, y);
|
||||
y += 5;
|
||||
doc.setFontSize(9);
|
||||
doc.setFont("helvetica", "normal");
|
||||
if (med.currentDose) {
|
||||
doc.text(
|
||||
t("doctorReport.glp1CurrentDose", {
|
||||
value: num(med.currentDose.value, 2),
|
||||
unit: med.currentDose.unit,
|
||||
since: new Date(med.currentDose.since).toLocaleDateString(locale),
|
||||
}),
|
||||
margin,
|
||||
y,
|
||||
);
|
||||
y += 5;
|
||||
}
|
||||
if (med.compliance.total > 0) {
|
||||
const rate = (med.compliance.taken / med.compliance.total) * 100;
|
||||
doc.text(
|
||||
t("doctorReport.glp1Compliance", {
|
||||
taken: med.compliance.taken,
|
||||
total: med.compliance.total,
|
||||
rate: num(rate, 1),
|
||||
}),
|
||||
margin,
|
||||
y,
|
||||
);
|
||||
y += 5;
|
||||
}
|
||||
if (med.doseHistory.length > 0) {
|
||||
const historyRows = med.doseHistory.map((dc) => [
|
||||
new Date(dc.effectiveFrom).toLocaleDateString(locale),
|
||||
`${num(dc.value, 2)} ${dc.unit}`,
|
||||
dc.note ?? "",
|
||||
]);
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
head: [
|
||||
[
|
||||
t("doctorReport.colGlp1Date"),
|
||||
t("doctorReport.colGlp1Dose"),
|
||||
t("doctorReport.colGlp1Note"),
|
||||
],
|
||||
],
|
||||
body: historyRows,
|
||||
theme: "grid",
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 3,
|
||||
textColor: [30, 30, 30],
|
||||
lineColor: [200, 200, 200],
|
||||
lineWidth: 0.3,
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [245, 245, 245],
|
||||
textColor: [30, 30, 30],
|
||||
fontStyle: "bold",
|
||||
},
|
||||
alternateRowStyles: { fillColor: [252, 252, 252] },
|
||||
margin: { left: margin, right: margin },
|
||||
});
|
||||
y =
|
||||
(doc as jsPDF & { lastAutoTable: { finalY: number } }).lastAutoTable
|
||||
.finalY + 6;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.glp1.sideEffects.length > 0) {
|
||||
if (y > 240) {
|
||||
doc.addPage();
|
||||
y = margin;
|
||||
}
|
||||
doc.setFontSize(11);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(t("doctorReport.glp1SideEffectsTitle"), margin, y);
|
||||
y += 5;
|
||||
const seRows = data.glp1.sideEffects.map((s) => [s.tag, String(s.count)]);
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
head: [[t("doctorReport.colGlp1SideEffect"), t("doctorReport.colCount")]],
|
||||
body: seRows,
|
||||
theme: "grid",
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 3,
|
||||
textColor: [30, 30, 30],
|
||||
lineColor: [200, 200, 200],
|
||||
lineWidth: 0.3,
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [245, 245, 245],
|
||||
textColor: [30, 30, 30],
|
||||
fontStyle: "bold",
|
||||
},
|
||||
alternateRowStyles: { fillColor: [252, 252, 252] },
|
||||
margin: { left: margin, right: margin },
|
||||
});
|
||||
y =
|
||||
(doc as jsPDF & { lastAutoTable: { finalY: number } }).lastAutoTable
|
||||
.finalY + 8;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.mood) {
|
||||
if (y > 240) {
|
||||
doc.addPage();
|
||||
|
||||
Reference in new issue
Block a user