From f45adb29f0d79a3d4391e6481c79197c47d24228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 13:56:08 +0200 Subject: [PATCH] feat(doctor-report): GLP1 therapy section with titration + weight curve + side-effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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*`. --- messages/de.json | 9 ++ messages/en.json | 9 ++ src/lib/doctor-report-data.ts | Bin 15518 -> 20467 bytes src/lib/doctor-report-pdf-core.ts | 141 ++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+) diff --git a/messages/de.json b/messages/de.json index eb0dd4835..5daba7282 100644 --- a/messages/de.json +++ b/messages/de.json @@ -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", diff --git a/messages/en.json b/messages/en.json index 47a49442b..73425ae02 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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", diff --git a/src/lib/doctor-report-data.ts b/src/lib/doctor-report-data.ts index 97974e4b304811e22d712546d476da85ccb5fcf9..342af7e626cc8d1c982323fcee6599afecd16283 100644 GIT binary patch delta 4779 zcmai2&5s;M6;}kpBnN~P<25-wH<9Tfv%Owq^Rb)t%JFWTK>VTX21~3~xTd>irn5cW zt*V+C1~Y0T5J!XeJd4q_)MVS=ZP?1X#*tL*e*Gwj9ES#E0^DNsDBQ10DlA?+=<%QGaj3|}Vu}Vwc zN+BP)-~(pj(?X?YMa38g#l<`~dJxf5wo6OHwmB_Nj^3w1=IYJzv0jbL7_>#|OBdPXjo~NeS zfsi38^&_q6=ag^3v|3dH9Ur(<>}`H~<$BfM?P3jpy|0Q{tz{qJF|AH$ghE>1t#xLA z&WHJ@4$o|00yMXqb=p3$Id-%EvR~CMHW_6FMqJ3e_0SYXWc4Gh-MRJo4o^oiGvbGn zM#!DxB%Q>G5z1VWIPZ!a=gB1j_nv`xY;o8o3|8VipD72==6Gk^`o(Wv|9Go*krS`u zsZ8dXgchUba_GJ1S35hj*o|&S-@ZdnJz+>Ok!m|DL_(amx`u#aC1bAhH0MYm=f>70 ztHB4F#)U$mtSnnppo8ik?MisYAp#*AubAAM=MUOpi-DA zrJ6=7Bexr{Td2G9t+DrQX~WOw6%`6q^WKUx`x3wcq2mB3U3VklygFXML*Cyph?vm*iMqkka9_JatueF?wvX}b++=n994E}!~ zpnH#wY{ufEsyPxIHko9M5GX?8nDR@{xTQ)LA_hT%?-Y8=9tb@tVo>$Al4YSxA9=Y1 zXq+O3Ma#nt>&skNA52lv$ab>vJVOtuQCGJWr(}Yr@{Fm`CGZ*Yo5oY?0JoTiZ}ml5 zYGPhh($Ojm29fjDhm7Nc4#agdPBSBatio_Mpo6U^D3epD$wr%YO1Fr83 zRyZS$tm_|J&yN81vw+uwbLeM)v2Z%3aP!yOSF4*ho8zNQ<`Y!KUAi^!fOYnH>OmZl z)&mMht5bBQ=;`r?4?Z}h!$UedA4ED!V;NdY!1dOz9%W%;SI^n4^TTK%Dl`GsP}|kt zYN_nyja@CZ$=kGR)%{Oxo?Ynx{_}?wRa4_Bm2M!Mw*C+jcZNH@UiZ6*4(TDtBT^VF zk}&LwZr3h4mL6c{*+ndO@VD9RFtY!KUD&C1=%h0qfQ%gnYYGc4Cp%2diXv&C3Fz18 zOy4<$C?P`*O zu%}(QF(f8)*@}p=lqNNmL=(}vUB@nEMswBChNVQHJGDZee>RfYg-lJmggFgUTsfZS zlXfwgt9UxbxYI7pKL0F9C(Yi9F%x)WU`gkdeWQzHgi_CC$DRuugKjyK>SxXJd9Oy= zINrB|s*mJDQNHV%@(`fp5$SGK7ZnY&OPZQ`TXsXW};8yO)QYdX{;0Gsl^k;a-SgAecz)giws_k?bx~+po zBbSm}i)Nw>!^IxPz>B1}8Q@ZM5(Ye;jlSKY}R zj^_IbThL~#&RJG0(b*38ZDfj%`02i=bVIKkVP^0fEqYF&C_;Y??cTwFDcH{bwG9~C zn(@y;Ey88Ui4Q5f^b$?Y>teWo#j1B`x5@C@dbD;d#@z+h)^TmeG+?UkZ7y`KlhuNa zyI?G|L3F=}CqzGVo%Nogta9CnslMMaI2sO{ZicthBrEHVVCXlm+Qt1Jhqq$@PUZ`Y z-O!Bjx2_Dqh#28FzYK#-ytTFYdz0#i-z6-GT`9IH zV@3Y%mc*~J+*$44_bj${So7ZEp8(tq4T6^}w&vj`X)kRM;4h8e(6Sy3x{v+qTc?yk zaQ0?rQGwRpK7k8yLA%#a7ITty_NKM8{o}5Kmv(RdacAqOmEtPni(*HvH)nTEcDr0c zh?eZGQP|cRd zqcN`4mk9%NDNh>Vpk>5JV`dCPF{F`&(dZ1dyG;WBb%ZSUpMmR!%v$)h=GM LND>6Ib4-8(M1~Y% diff --git a/src/lib/doctor-report-pdf-core.ts b/src/lib/doctor-report-pdf-core.ts index 21826fa09..f80bfa562 100644 --- a/src/lib/doctor-report-pdf-core.ts +++ b/src/lib/doctor-report-pdf-core.ts @@ -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();