@medicus.ai/medicus-report-pdf-generator 1.3.13 → 1.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/.vscode/settings.json +3 -3
  2. package/HANDOVER.md +146 -0
  3. package/README.md +26 -26
  4. package/app/i18n.config.js +20 -20
  5. package/app/i18n_wellbeing.config.js +20 -20
  6. package/app/services/localeService.js +28 -28
  7. package/assets/Mediclinic/css/nasco_report.css +6297 -6297
  8. package/assets/Mediclinic/css/nasco_report_rtl.css +851 -851
  9. package/assets/Mediclinic/css/style.css +33 -33
  10. package/assets/Mediclinic/js/js.js +112 -112
  11. package/assets/Mediclinic/js/raphael.js +8437 -8437
  12. package/assets/arabic.css +275 -275
  13. package/assets/charts.min.js +7 -7
  14. package/assets/corporate_report/css/report.css +582 -582
  15. package/assets/corporate_report/css/report_rtl.css +144 -144
  16. package/assets/corporate_report/js/js.js +14 -14
  17. package/assets/data/data.json +124 -124
  18. package/assets/data/data2.json +675 -675
  19. package/assets/data/pdf_data_with_history.json +1255 -1255
  20. package/assets/doctor-note-insight-icon.svg +11 -11
  21. package/assets/imc/css/style.css +61 -61
  22. package/assets/jquery-1.4.min.js +151 -151
  23. package/assets/jquery-2.1.0.min.js +3 -3
  24. package/assets/js.js +201 -201
  25. package/assets/medicus_pdf/arabic.css +444 -444
  26. package/assets/medicus_pdf/styles.css +2427 -2427
  27. package/assets/print.css +4 -4
  28. package/assets/qr-code-style.css +68 -68
  29. package/assets/qrcode.min.js +3 -3
  30. package/assets/raphael-min.js +10 -10
  31. package/assets/sanusx/css/sanusx_report.css +707 -707
  32. package/assets/sizing.js +2 -2
  33. package/assets/styles.css +1545 -1545
  34. package/assets/styles.min.css +1358 -1358
  35. package/assets/translation.js +30 -30
  36. package/assets/wellbeing/css/nasco_report.css +3472 -3472
  37. package/assets/wellbeing/css/nasco_report_rtl.css +679 -679
  38. package/assets/wellbeing/js/js.js +111 -111
  39. package/assets/wellbeing/js/raphael.js +8437 -8437
  40. package/base64/cairo-font.js +2 -2
  41. package/config/Mediclinic.json +37 -37
  42. package/config/Najeeb.ai.json +35 -35
  43. package/config/Pha.json +64 -64
  44. package/config/bionext.json +37 -37
  45. package/config/default.json +35 -35
  46. package/config/diagnostikare.json +34 -34
  47. package/config/maisonsante.json +39 -39
  48. package/config/nasco.json +35 -35
  49. package/config/sanitas.json +35 -35
  50. package/config/sanusx.json +34 -34
  51. package/config/test.txt +4 -4
  52. package/data converter.js +53 -53
  53. package/docs/01-architecture-overview.md +149 -0
  54. package/docs/02-configuration-and-whitelabelling.md +111 -0
  55. package/docs/03-templating-and-rendering.md +71 -0
  56. package/docs/04-report-pipelines.md +128 -0
  57. package/docs/05-questions-and-data-model.md +67 -0
  58. package/docs/06-internationalization.md +59 -0
  59. package/docs/07-email-notifications.md +59 -0
  60. package/docs/08-known-issues-and-technical-debt.md +41 -0
  61. package/docs/README.md +22 -0
  62. package/downloadfile.js +6 -6
  63. package/example.txt +1188 -1188
  64. package/index.js +481 -480
  65. package/lib/big_integral_questionnaire.js +370 -298
  66. package/lib/corporate_report_generator.js +399 -399
  67. package/lib/pdf_generator.min.js +525 -525
  68. package/lib/sanusx_report_generator.js +533 -533
  69. package/lib/sendEmail.js +343 -343
  70. package/lib/template.js +2227 -2227
  71. package/lib/wellbeing_report_generator.js +2164 -2134
  72. package/locales/ar-AE.json +169 -169
  73. package/locales/ar-SA.json +169 -169
  74. package/locales/ar.json +397 -397
  75. package/locales/de.json +169 -169
  76. package/locales/en.json +229 -229
  77. package/locales/fr.json +168 -168
  78. package/locales/pt.json +165 -165
  79. package/locales/wellbeing/ar-AE.json +149 -149
  80. package/locales/wellbeing/ar-SA.json +257 -257
  81. package/locales/wellbeing/de.json +301 -301
  82. package/locales/wellbeing/en.json +423 -423
  83. package/locales/wellbeing/es.json +133 -133
  84. package/locales/wellbeing/fr.json +121 -121
  85. package/locales/wellbeing/it-IT.json +121 -121
  86. package/locales/wellbeing/pt.json +121 -121
  87. package/locales/wellbeing/tr.json +121 -121
  88. package/locales/wellbeing/zh-CN.json +121 -121
  89. package/locales/zh-CN.json +165 -165
  90. package/package.json +47 -47
  91. package/preview-big-integral.js +75 -75
  92. package/run.js +15 -15
  93. package/templates/Mediclinic/blocks/first-section.html +205 -205
  94. package/templates/Mediclinic/blocks/footer.html +7 -7
  95. package/templates/Mediclinic/blocks/header-template.html +5 -5
  96. package/templates/Mediclinic/blocks/header.html +20 -20
  97. package/templates/Mediclinic/blocks/patient-table.html +38 -38
  98. package/templates/Mediclinic/blocks/pdf-header.html +9 -9
  99. package/templates/Mediclinic/blocks/tips.html +8 -8
  100. package/templates/Mediclinic/ltr_no_pages.html +37 -37
  101. package/templates/Mediclinic/rtl_no_pages.html +42 -42
  102. package/templates/Mediclinic/wellbeing_template.html +41 -41
  103. package/templates/Pha/blocks/first-section.html +207 -207
  104. package/templates/Pha/blocks/footer.html +7 -7
  105. package/templates/Pha/blocks/header-template.html +5 -5
  106. package/templates/Pha/blocks/header.html +20 -20
  107. package/templates/Pha/blocks/patient-table.html +38 -38
  108. package/templates/Pha/blocks/pdf-header.html +8 -8
  109. package/templates/Pha/blocks/tips.html +8 -8
  110. package/templates/Pha/ltr_no_pages.html +37 -37
  111. package/templates/Pha/rtl_no_pages.html +42 -42
  112. package/templates/Pha/wellbeing_template.html +41 -41
  113. package/templates/base.html +23 -23
  114. package/templates/blocks/ar-first-page-header.html +68 -68
  115. package/templates/blocks/ar-footer.html +54 -54
  116. package/templates/blocks/ar-header.html +67 -67
  117. package/templates/blocks/biomarker-compact-ar.html +27 -27
  118. package/templates/blocks/biomarker-compact.html +27 -27
  119. package/templates/blocks/biomarker-details.html +41 -41
  120. package/templates/blocks/biomarker-insight.html +22 -22
  121. package/templates/blocks/biomarker-min.html +9 -9
  122. package/templates/blocks/doctor-note.html +11 -11
  123. package/templates/blocks/footer.html +35 -35
  124. package/templates/blocks/header-first-page.html +46 -46
  125. package/templates/blocks/header.html +49 -49
  126. package/templates/blocks/panel-compact.html +38 -38
  127. package/templates/blocks/panel-details.html +16 -16
  128. package/templates/blocks/report-summary.html +16 -16
  129. package/templates/blocks/section-title.html +4 -4
  130. package/templates/blocks/signature.html +16 -16
  131. package/templates/blocks/summary-insight.html +15 -15
  132. package/templates/blocks/summary.html +9 -9
  133. package/templates/corporate_report/cover_page.html +23 -23
  134. package/templates/corporate_report/footer.html +9 -9
  135. package/templates/corporate_report/ltr_no_pages.html +20 -20
  136. package/templates/corporate_report/page.html +24 -24
  137. package/templates/corporate_report/participant_analytics.html +5 -5
  138. package/templates/corporate_report/rtl_no_pages.html +20 -20
  139. package/templates/empty.html +10 -10
  140. package/templates/first_page_head.html +157 -157
  141. package/templates/imc/first-header-template.html +53 -53
  142. package/templates/ltr.html +1368 -1368
  143. package/templates/ltr_no_pages.html +89 -89
  144. package/templates/maisonsante/blocks/first-section.html +151 -207
  145. package/templates/maisonsante/blocks/footer.html +7 -7
  146. package/templates/maisonsante/blocks/header-template.html +5 -5
  147. package/templates/maisonsante/blocks/header.html +20 -20
  148. package/templates/maisonsante/blocks/patient-table.html +38 -38
  149. package/templates/maisonsante/blocks/pdf-header.html +8 -8
  150. package/templates/maisonsante/blocks/tips.html +8 -8
  151. package/templates/maisonsante/ltr_no_pages.html +38 -38
  152. package/templates/maisonsante/rtl_no_pages.html +43 -43
  153. package/templates/maisonsante/wellbeing_template.html +41 -41
  154. package/templates/no_pages.html +1224 -1224
  155. package/templates/popup/popup-template.html +129 -129
  156. package/templates/rtl_no_pages.html +92 -92
  157. package/templates/sanusx/blocks/footer.html +6 -6
  158. package/templates/sanusx/blocks/header.html +20 -20
  159. package/templates/sanusx/blocks/personal-details.html +123 -123
  160. package/templates/sanusx/blocks/predictions-section.html +57 -57
  161. package/templates/sanusx/blocks/recommendations-section.html +6 -6
  162. package/templates/sanusx/blocks/super-power-section.html +29 -29
  163. package/templates/sanusx/blocks/tips.html +8 -8
  164. package/templates/sanusx/ltr_no_pages.html +26 -26
  165. package/templates/sanusx/rtl_no_pages.html +36 -36
  166. package/templates/template.html +1377 -1377
  167. package/templates/wellbeing/blocks/first-section.html +106 -106
  168. package/templates/wellbeing/blocks/footer.html +7 -7
  169. package/templates/wellbeing/blocks/header.html +37 -37
  170. package/templates/wellbeing/blocks/tips.html +8 -8
  171. package/templates/wellbeing/ltr_no_pages.html +35 -35
  172. package/templates/wellbeing/rtl_no_pages.html +36 -36
  173. package/templates/wellbeing/wellbeing_template.html +43 -43
  174. package/test.js +74 -74
  175. package/testing-reports/mediclinic/arabic-data.js +0 -439
  176. package/testing-reports/mediclinic/doctor-data.js +0 -523
  177. package/testing-reports/mediclinic/with-wellbeing.js +0 -415
  178. package/testing-reports/pha/notes-example.js +0 -397
@@ -0,0 +1,149 @@
1
+ # 1. Architecture Overview
2
+
3
+ ## 1.1 What this package does
4
+
5
+ `@medicus.ai/medicus-report-pdf-generator` (`package.json:2`) converts JSON health-report data into PDF documents for several distinct products:
6
+
7
+ - **Core "Medicus" lab report** (biomarkers/panels/insights) — module `lib/pdf_generator.min.js`, called via `generateMedicusPDF`
8
+ - **Wellbeing report** (questionnaire scores, PHQ‑9/GAD‑7 style) + optional merged lab "SmartReport" — module `lib/wellbeing_report_generator.js`, called via `generateNascoPDF` and `generateFullPdf`
9
+ - **Corporate/aggregate analytics report** (population-level, for employers) — module `lib/corporate_report_generator.js`, called via `generateCorporateReportPDF`
10
+ - **SanusX consumer wellbeing report** ("Health Hero" avatar report) — module `lib/sanusx_report_generator.js`, called via `generateSanuxPDF`
11
+
12
+ All four are independent implementations. They do **not** share a common rendering module — each has its own copy of the "read HTML fragment from disk → string-replace `{{tokens}}` → load into jsdom → manipulate with jQuery → serialize → hand to Puppeteer" pattern. See [03-templating-and-rendering.md](03-templating-and-rendering.md).
13
+
14
+ ## 1.2 Directory map
15
+
16
+ ```
17
+ index.js Public API — the only file a host app requires
18
+ run.js Optional child-process wrapper around generateMedicusPDF
19
+ lib/
20
+ pdf_generator.js Core report renderer — SOURCE, but NOT what actually runs (see 1.4)
21
+ pdf_generator.min.js Core report renderer — the file index.js actually requires
22
+ wellbeing_report_generator.js Wellbeing + SmartReport renderer, plus PDF conversion for it
23
+ big_integral_questionnaire.js Renders one static "Big Integral Questionnaire" table (Maison Sante only)
24
+ corporate_report_generator.js Corporate/analytics report renderer
25
+ sanusx_report_generator.js SanusX report renderer
26
+ template.js Shared rendering helpers used ONLY by wellbeing_report_generator.js
27
+ (biomarker/panel/insight HTML builders, V3-suffixed functions)
28
+ sendEmail.js Nodemailer wrapper, used by the wellbeing/corporate/sanusx flows
29
+ app/
30
+ i18n.config.js i18n config for the core "Medicus" report (locales/)
31
+ i18n_wellbeing.config.js i18n config for wellbeing/corporate/sanusx (locales/wellbeing/)
32
+ services/localeService.js Thin wrapper exposing .t(key) / .setLocale(lang)
33
+ config/ One JSON per brand (whitelabelling) + default.json
34
+ templates/ HTML fragments — shared (templates/blocks) + per-brand folders
35
+ locales/ Translation JSON — main + locales/wellbeing subfolder
36
+ assets/ CSS/JS/fonts/images served into the rendered HTML, some per-brand
37
+ output/ Default write location for generated PDFs/HTML/logs (not cleaned up)
38
+ testing-reports/ Example payloads used by test.js / preview scripts
39
+ tests/ Ad hoc test scripts (not a real test runner/framework)
40
+ ```
41
+
42
+ ## 1.3 Entry points (`index.js`)
43
+
44
+ `index.js` exports a flat object of async functions. There is no class, no server, no routing — just functions a host app calls directly.
45
+
46
+ - **`generateHTMLStaging`** — pipeline: Core. Build HTML only (re-exported straight from `pdf_generator.min.js`).
47
+ - **`generateMedicusPDF(base64Object, isDebugging, isDownloadable, onlyHTML)`** — pipeline: Core. Full core lab report → PDF.
48
+ - **`generateNascoPDF(data, isDebugging, isDownloadable, shouldSendEmail)`** — pipeline: Wellbeing. Plain wellbeing report, optionally emailed.
49
+ - **`generateFullPdf(data, isDebugging, isDownloadable, shouldSendEmail, selectedLabs, showHeaderLogo)`** — pipeline: Wellbeing (extended). Branded wellbeing report + merged SmartReport + **encryption**.
50
+ - **`generateSanuxPDF(data, isDebugging, isDownloadable, shouldSendEmail)`** — pipeline: SanusX. SanusX consumer report, optionally emailed.
51
+ - **`generateCorporateReportPDF(json, isDebugging, isDownloadable)`** — pipeline: Corporate. Corporate/analytics report.
52
+ - **`sendEmail(json)`** — fire a generic notification email (no PDF pipeline).
53
+ - **`generateQrCode(json)`** — pipeline: Core (`generatePatientQR`). Rasterizes a caller-supplied HTML string (e.g. an inline SVG QR code) to PDF — despite the name, does not generate a QR code itself.
54
+
55
+ **Payload convention:** every PDF-producing function expects the "real" data wrapped one level up: the caller passes a JSON *string* whose parsed object has a `data` field containing **base64-encoded JSON** (the actual report content), plus sibling fields like `client`, `language`, `host`/`port`/`authUser`/`authPass`/`sendFromEmail`/`secure` (SMTP config, only used if emailing) and, for `generateFullPdf`, `pdfPassword`. `generateMedicusPDF` is the odd one out — it takes the base64 payload directly as its first argument rather than nested inside a JSON envelope.
56
+
57
+ ```js
58
+ // Illustrative shape of the outer envelope for generateNascoPDF / generateFullPdf / generateSanuxPDF
59
+ {
60
+ "data": "<base64-encoded JSON string — the actual report payload>",
61
+ "client": "Mediclinic", // whitelabel selector, see doc 2
62
+ "language": "en", // or "ar", "ar-AE", "de", ...
63
+ "host": "smtp.example.com", // only read if shouldSendEmail is true
64
+ "port": 587,
65
+ "authUser": "...",
66
+ "authPass": "...",
67
+ "sendFromEmail": "noreply@...",
68
+ "secure": true, // NOTE: currently ignored, see doc 7
69
+ "pdfPassword": "..." // generateFullPdf only — PDF is encrypted with this
70
+ }
71
+ ```
72
+
73
+ Every function decodes with `Buffer.from(base64Object, 'base64').toString('utf8')` then `JSON.parse` — this is **encoding, not encryption**; the payload (including any SMTP credentials riding alongside it) is trivially recoverable by anyone who can see the request. See [07-email-notifications.md](07-email-notifications.md) for the security implication.
74
+
75
+ `run.js` is a thin optional wrapper that runs `generateMedicusPDF` in a forked child process (`process.on('message', ...)` / `process.send(...)`), for host apps that want report generation isolated from their main event loop. It is not required — most callers use `index.js` directly.
76
+
77
+ ## 1.4 Critical fact: the core report renderer that actually runs is `pdf_generator.min.js`, not `pdf_generator.js`
78
+
79
+ `index.js:1` requires `./lib/pdf_generator.min` — **not** `./lib/pdf_generator`. These are commonly assumed to be a source-file/build-artifact pair (the kind that a bundler regenerates), but they are not:
80
+
81
+ - There is **no build script** anywhere in the repo that produces `pdf_generator.min.js` from `pdf_generator.js` (no `webpack.config.js` exists, despite `webpack` being a devDependency).
82
+ - Git history shows the two files are edited **completely independently**: `lib/pdf_generator.js` was last touched by commit `f31298e` ("fix range") on **2021-03-25**. `lib/pdf_generator.min.js` was last touched by commit `8bb73da` ("Reformat Code") on **2025-07-30** — over four years later — and that commit only reformatted/beautified the minified file (it went from a single packed line to ~525 readable lines); it did not re-minify from `pdf_generator.js`, which was untouched.
83
+ - Today the two files happen to be logically equivalent in most places (verified by spot comparison), but this is coincidental, not enforced by any tooling. **A change made only to `lib/pdf_generator.js` will never ship** — `index.js` never loads it.
84
+
85
+ **Practical rule: when working on the core "Medicus" lab-report pipeline, edit `lib/pdf_generator.min.js`.** Treat `lib/pdf_generator.js` as a legacy reference copy at best, and flag it for removal or for wiring up an actual build step. All other generators (`wellbeing_report_generator.js`, `corporate_report_generator.js`, `sanusx_report_generator.js`) do not have this problem — each is required directly from its single, non-minified source file.
86
+
87
+ ## 1.5 Data flow, at a glance
88
+
89
+ ```
90
+ Host app This package Output
91
+ ───────── ──────────── ──────
92
+ JSON payload ──base64──▶ index.js export
93
+ │
94
+ ▼
95
+ decode base64 → JSON.parse
96
+ │
97
+ ▼
98
+ generateHTML*(data, isDebugging, client, language)
99
+ - loads config/{client}.json (branding) ┐
100
+ - loads templates/{brand or shared}/*.html │ see doc 2 & 3
101
+ - loads locales/{...}/{language}.json │
102
+ - fs.readFileSync HTML fragments, │
103
+ string .replace("{{token}}", value) │
104
+ - loads shell HTML into jsdom + jQuery │
105
+ - DOM-injects rendered fragments ┘
106
+ │
107
+ ▼ (HTML string, plus metadata: header/footer HTML, output file path)
108
+ generatePDF*(html) — Puppeteer
109
+ - page.setContent / page.goto(file://...)
110
+ - page.pdf({ headerTemplate, footerTemplate, margin, ... })
111
+ - (core report only) two page.pdf() calls merged via hummus,
112
+ to give page 1 a different header than the rest
113
+ │
114
+ ▼ PDF Buffer
115
+ (generateFullPdf only) node-qpdf2 encrypts the buffer with reportData.pdfPassword
116
+ │
117
+ ▼
118
+ return Buffer (isDownloadable) | base64 string | sendNascoEmail(...) result
119
+ ```
120
+
121
+ ## 1.6 Key third-party dependencies and why they're there
122
+
123
+ - **`puppeteer`** (pinned `^1.15.0`, very old) — headless Chrome → PDF rendering, in all four generators. Each generator launches its own Puppeteer instance independently — no shared "renderPdf" helper.
124
+ - **`hummus`** (`1.0.111`) — merging two separately-rendered PDF buffers (core report only, to give page 1 a distinct header). Effectively unmaintained upstream.
125
+ - **`node-qpdf2`** — encrypting the final PDF with a password (`generateFullPdf` only). Dynamically `import()`-ed (ESM) inside a CommonJS file.
126
+ - **`jsdom` + `jquery`** — server-side DOM construction and manipulation before handing HTML to Puppeteer. Used by every generator except the newer `template.js` V3 functions, which build HTML via plain string concatenation instead.
127
+ - **`i18n`** (`^0.8.3`, resolves to `0.8.6`) — translation string lookup. Two independently configured instances — see doc 6.
128
+ - **`nodemailer`** — sending report emails with the PDF attached. Transport config comes entirely from the caller's payload, not env vars.
129
+ - **`chart.js`** (npm) — declared but **not actually used server-side** — dead import in `pdf_generator.js`. The real chart rendering uses a separately bundled copy, `assets/charts.min.js`, executed **inside the headless Chrome page** before the PDF snapshot is taken. Two independent copies of Chart.js that can drift out of sync.
130
+ - **`qr-image` / `qrcode`** (npm) — also effectively unused server-side for the in-report PIN/QR box — that box is rendered client-side via the bundled `assets/qrcode.min.js`. `generatePatientQR` in `index.js`/`pdf_generator.min.js` is a generic "rasterize this HTML string" utility, not a QR generator itself.
131
+ - **`canvas`, `get-canvas-context`, `self-adapt-fontsize`, `textfit`, `big-text.js`** — legacy font-fitting/canvas helpers. Present in `package.json` but not central to the current rendering path — treat as legacy.
132
+
133
+ ## 1.7 File/temp-file handling — an evolving pattern worth knowing
134
+
135
+ Older code paths (`generateMedicusPDF`, `generateNascoPDF`, `generateSanuxPDF`, the internals of `pdf_generator.js`/`.min.js`) write intermediate HTML and the final PDF to **fixed, shared filenames** inside the package's own `output/` directory (e.g. `output/sample.pdf`, `output/nasco-sample.pdf`, `output/LOGS.txt`) and never delete them. Two concurrent requests through the same process can clobber each other's files.
136
+
137
+ `generateFullPdf` (`index.js:194-314`) is the one flow that was hardened against this: it generates a `crypto.randomUUID()` per call and builds all temp paths (`wb-{callId}-in.pdf`, `wb-{callId}-enc.pdf`, `wb-{callId}-mail.pdf`) inside `os.tmpdir()`, then cleans them up in a `finally` block via a best-effort `safeUnlink`. **If you add a new PDF-producing flow, follow this newer pattern, not the older fixed-filename one.**
138
+
139
+ ## 1.8 Appendix: other root-level files (accounted for, not part of the runtime pipeline)
140
+
141
+ A handful of files at the repo root are not required by `index.js` and are not wired into any of the four report pipelines. Listed here so nothing is silently unexplained:
142
+
143
+ - **`test.js`**, **`tests/test.js`**, **`tests/test-qr-code.js`**, **`preview-big-integral.js`** — ad hoc developer scripts, not a test framework. See [`HANDOVER.md` §9](../HANDOVER.md#9-runningtesting-this-locally) for how to use them.
144
+ - **`downloadfile.js`** — a one-off helper that fetches a pinned Chromium build for Puppeteer on Windows. Not required if Puppeteer's own Chromium download already succeeded during `npm install`.
145
+ - **`data converter.js`** (53 lines) — a standalone script that parses an ad hoc "Antimicrobial Agent / Sensitivity" text format into structured rows. Not `require`d by anything in `lib/`, `index.js`, or `run.js` — an orphaned, one-off data-conversion utility from some prior integration, not part of the active rendering pipeline.
146
+ - **`base64/cairo-font.js`** — a 2-line file containing a single, large base64-encoded font data URI. Not `require`d anywhere — orphaned/unused; the fonts actually used at render time are the files under `assets/fonts/`.
147
+ - **`example.txt`** — a large (1000+ line) saved fragment of previously-rendered report HTML, kept as a manual reference/comparison sample, not consumed by any code.
148
+
149
+ None of these affect report output; they're safe to leave alone or clean up opportunistically.
@@ -0,0 +1,111 @@
1
+ # 2. Configuration & Whitelabelling
2
+
3
+ Whitelabelling ("which brand does this report look like") is driven entirely by a **runtime string** — a `client` (or, for the corporate report, `clientName`) field in the request payload. There is no environment variable, no CLI flag, and no central "client registry" module. Each generator independently maps that string to a config JSON file and a template folder, and — this is the single most important thing to understand about this system — **the four generators do it four slightly different, inconsistent ways.**
4
+
5
+ ## 2.1 The `config/` directory
6
+
7
+ One JSON file per brand, named after the client string, plus `config/default.json` as the fallback:
8
+
9
+ ```
10
+ config/
11
+ default.json
12
+ Mediclinic.json
13
+ Pha.json
14
+ Najeeb.ai.json
15
+ bionext.json
16
+ diagnostikare.json
17
+ maisonsante.json
18
+ nasco.json (referenced by filename convention; see 2.2 for how "nasco" resolves)
19
+ sanitas.json
20
+ sanusx.json
21
+ ```
22
+
23
+ ### Common schema (present in most/all files)
24
+
25
+ - **`logo`** — Logo image, URL or base64 data-URI
26
+ - **`body_icon`, `lifestyle_icon`, `mind_icon`, `bulb_icon`** — Section icons, URL or base64 data-URI
27
+ - **`first-level-color` … `fifth-level-color`** — 5-step color ramp used for score/chart coloring
28
+ - **`general-font-color`, `disclaimer-text-color`, `top-box-background`, `footer-background`** — Theme colors
29
+ - **`body_font_color`, `list_icons_font_color`, `insights_font_color`** — More theme colors
30
+ - **`signature`** — Free text signed off in emails/footers, e.g. `"Team Medicus"`, `"Team Sanitas"`
31
+ - **`corporate_report`** — Nested object, see 2.1.1
32
+
33
+ ### Brand-specific keys
34
+
35
+ - `title` — only `Mediclinic.json`, `Pha.json`, `maisonsante.json` (`"HealthRiskAssessment"`).
36
+ - `score-header-color`, `score-description-color`, `svg-icon-color`, `low-score-text-color`, `section-title-border-color`, `first-section-scores-border-color`, `footer-border-color` — only `Mediclinic.json`, `Pha.json`, `maisonsante.json`.
37
+ - `labs-logo` / `labs-logo-aliases` — **only `Pha.json`**. `labs-logo` maps a canonical lab key (e.g. `"labcorp"`, `"wp"`) to a base64 logo image. `labs-logo-aliases` maps the same keys to arrays of alternate spellings (e.g. `["lab corp", "lab_corp", ...]`) used to match whatever spelling the caller sends in `selectedLabs` (see doc 4, wellbeing SmartReport header logos).
38
+ - `big_integral_questionnaire` — only `Pha.json` and `maisonsante.json`: `{ title, showHormonalFemale, showHormonalMale, colors: { headerBg, subheaderBg, headerText, rowBorder, elevatedText } }`. Consumed by `lib/big_integral_questionnaire.js` (see doc 5 — this module currently renders 100% mock data).
39
+
40
+ #### 2.1.1 The nested `corporate_report` object
41
+
42
+ ```json
43
+ "corporate_report": {
44
+ "logo": "...",
45
+ "main_color": "rgb(0, 0, 153)",
46
+ "report_date_color": "rgb(139, 139, 167)",
47
+ "section_title_color": "rgb(0, 0, 153)",
48
+ "section_title_border_color": "rgb(0, 158, 226)",
49
+ "chart_title": "rgb(139, 139, 167)",
50
+ "summary_background": "#e5f5fc",
51
+ "relation_content_border_color": "rgba(0, 0, 153, 0.3)",
52
+ "relation_content_bg": "#f2f2ff",
53
+ "table_tr_even_bg": "rgb(237, 237, 248)",
54
+ "recommends_bg": "rgba(0, 0, 153, 0.05)",
55
+ "recommends_title": "Moxie",
56
+ "show_medicus_logo": true,
57
+ "show_second_footer_logo": true
58
+ }
59
+ ```
60
+
61
+ `show_second_footer_logo` is missing from `sanusx.json` and `diagnostikare.json`. `bionext.json`'s `corporate_report` block additionally has its own `body_font_color`, unique to that one file.
62
+
63
+ `Najeeb.ai.json` is essentially a duplicate of `default.json` (same colors, `signature: "Team Medicus"`) — a placeholder brand with no real customization yet. `sanitas.json` overrides `signature` ("Team Sanitas"), the color ramp, and `corporate_report.recommends_title` ("Sanitas") but otherwise mirrors the default.
64
+
65
+ ## 2.2 How `client`/`clientName` resolves to a config file — and why it's inconsistent
66
+
67
+ There is no shared config-loader. Each generator reimplements the lookup:
68
+
69
+ - **`wellbeing_report_generator.js`** (`loadClientConfig`) — Resolution logic: `'pha'` (any case) → `'Pha'`; otherwise uses the string as-is to build `config/{name}.json`; falls back to `config/default.json` via `fs.existsSync`. *Gotcha:* only `pha` gets special-cased; every other brand must be passed with exact on-disk casing.
70
+ - **`corporate_report_generator.js`** — Resolution logic: `client = data.clientName.toLowerCase()`, then `config/{client}.json`. *Gotcha:* **lower-cases before lookup**, but the actual files are capitalized (`Mediclinic.json`, `Pha.json`, `Najeeb.ai.json`). This only works on case-insensitive filesystems (Windows/macOS default). On a case-sensitive Linux filesystem, `clientName: "Mediclinic"` silently falls back to `default.json`.
71
+ - **`lib/sendEmail.js`** (`sendNascoEmail`) — Resolution logic: uses the **raw, unmodified** client string, no lower-casing, no `pha`→`Pha` mapping. *Gotcha:* caller must pass the exact on-disk filename casing, a different rule than the wellbeing generator it's normally called alongside.
72
+ - **`sanusx_report_generator.js`** — Resolution logic: **ignores `clientName` for config entirely** — unconditionally `require('../config/sanusx.json')`. *Gotcha:* the `client` parameter this generator accepts is only used later to add a CSS class (`$(".score-main").addClass(client)`), which currently matches no CSS rule — effectively a no-op. SanusX is single-brand in practice.
73
+ - **`lib/pdf_generator.js` / `.min.js`** (core report) — Resolution logic: does not touch `config/` at all. *Gotcha:* branding for the core report must arrive pre-baked inside the data payload from the host app — there is no per-client config file in this pipeline.
74
+
75
+ **Practical consequence:** if you're onboarding a new brand, check *which* generator you're using before assuming "just add `config/NewBrand.json`" is enough — confirm the exact casing rule for that specific generator, and remember `sanusx_report_generator.js` won't pick it up at all without a code change.
76
+
77
+ One more special case: `wellbeing_report_generator.js:1341-1353` hard-codes a feature gate — when `client.toLowerCase() === 'maisonsante' && data.IsDoctor`, it re-reads `config/maisonsante.json` a second time (bypassing the already-resolved config object) specifically to feed `big_integral_questionnaire.js`.
78
+
79
+ ## 2.3 Config → colors/branding, not template choice, in most flows
80
+
81
+ Loading `config/{client}.json` gives you colors/logos/icons that get injected as inline `<style>` overrides or `{{token}}` substitutions into whatever HTML template was already chosen (see 2.4) — it does **not**, by itself, choose which template folder is used. Two exceptions: the corporate report and SanusX generators always use one fixed template folder regardless of config; only the wellbeing "extended" (SmartReport) flow ties template folder selection directly to the client string.
82
+
83
+ ## 2.4 Template folder selection — no blocks-level fallback
84
+
85
+ `templates/` has full per-brand subfolders for **`Mediclinic`**, **`Pha`**, and **`maisonsante`** (each with `ltr_no_pages.html`, `rtl_no_pages.html`, `wellbeing_template.html` and its own `blocks/`), a generic **`wellbeing`** folder used as the default, a **`sanusx`** folder, plus non-brand folders `corporate_report/`, `imc/`, `popup/`, and the shared top-level `templates/blocks/`.
86
+
87
+ - **Core report** (`pdf_generator.min.js`) — always `templates/blocks/*` (+ `templates/imc/first-header-template.html` for one specific lab type). No per-brand folder is ever consulted; all differentiation happens through the data payload, not through swapped templates.
88
+ - **Wellbeing, plain** (`generateHTMLWellbeingReport` → `loadWellbeingTemplates`) — always `templates/wellbeing/` regardless of client. Brands going through this flow are differentiated purely by injected config colors/logo.
89
+ - **Wellbeing, extended/SmartReport** (`generateHTMLWellbeingReportWithSmartReport` → `loadExtendedTemplates`) — derives the folder name **directly from the client string** (`'pha'→'Pha'`, `'mediclinic'→'Mediclinic'`, else used as-is) → `templates/{ClientName}/`. Reads every block with `fs.readFileSync` and **no existence check** — if the folder or a block inside it is missing, it throws `ENOENT`. Only works today for clients with a dedicated folder: `Mediclinic`, `Pha`, `maisonsante`.
90
+ - **Corporate report** — always `templates/corporate_report/*`, regardless of client.
91
+ - **SanusX** — always `templates/sanusx/*`, regardless of client.
92
+
93
+ There is **no fallback from a brand folder to the shared `templates/blocks/`** for an individual missing block — brand folders are complete, parallel copies, not overrides layered on a shared base. If you add a fourth brand to the "extended" wellbeing flow, you must create a full `templates/{Brand}/` folder with every block file the existing three have, or the render will crash.
94
+
95
+ ## 2.5 Brand asset directories
96
+
97
+ `assets/Mediclinic/`, `assets/pha/labcrop-logo.png`, `assets/sanusx/`, `assets/imc/`, `assets/corporate_report/`, `assets/wellbeing/`, `assets/medicus_pdf/*.css`.
98
+
99
+ Notable: **`templates/Pha/*.html` and `templates/maisonsante/*.html` both reference `../assets/Mediclinic/css/...` and `../assets/Mediclinic/js/js.js`** — Pha and Maison Sante do not have their own CSS/JS bundle; they reuse the Mediclinic bundle wholesale and rely entirely on the `config/{Client}.json` color values for visual differentiation. `templates/corporate_report/cover_page.html` also hard-codes a Nasco logo image path regardless of client, unless overridden by config.
100
+
101
+ ## 2.6 Environment variables
102
+
103
+ There is exactly **one** environment variable read anywhere in the codebase: `SHOW_QR_BOX` (`lib/template.js`), which toggles whether the in-report PIN/QR box renders. It has nothing to do with whitelabelling. **No environment variable selects a client, config, or template.**
104
+
105
+ ## 2.7 Step-by-step trace: "given `client = X`, what actually loads?"
106
+
107
+ 1. Host app calls an `index.js` export with `client: X` (or `clientName: X` for the corporate report) inside the payload.
108
+ 2. The generator resolves `X` to `config/{mapped X}.json` if it exists, else `config/default.json` — using whichever mapping rule from §2.2 applies to that generator.
109
+ 3. Config values (colors, logo, icons, `corporate_report`/`big_integral_questionnaire` sub-objects) get inlined into the HTML as `<style>` overrides / `{{token}}` substitutions.
110
+ 4. The template folder is chosen per §2.4 — for three of the four generators this is *independent* of `X`; only the "extended" wellbeing flow ties folder choice to `X`.
111
+ 5. Assets are pulled in via relative `<link>`/`<script>` tags baked into whichever template was chosen — mostly `assets/Mediclinic`, `assets/wellbeing`, `assets/sanusx`, `assets/corporate_report`, or `assets/imc` — independent of `X` except for the config-driven inline color overrides.
@@ -0,0 +1,71 @@
1
+ # 3. Templating & Rendering Pipeline
2
+
3
+ ## 3.1 There is no template engine
4
+
5
+ Despite the `templates/` folder full of `.html` files, this project does **not** use Handlebars, EJS, Mustache, or any templating library. Every generator (core, wellbeing, corporate, sanusx) hand-rolls the same two techniques:
6
+
7
+ **(a) Plain string `{{token}}` replacement**, for fragments Puppeteer needs as raw strings (Chrome's `headerTemplate`/`footerTemplate` print options only accept static HTML strings, so these can't be manipulated after the fact):
8
+ ```js
9
+ headerTemplate = headerTemplate
10
+ .replace("{{logo}}", img)
11
+ .replace("{{patient_name}}", patientName)
12
+ .replace("{{report_ref}}", fitNumber(data.reportNumber))
13
+ // ...
14
+ ```
15
+
16
+ Block files contain literal `{{...}}` tokens, e.g. `templates/blocks/header.html` (`{{patient_name}}`, `{{report_ref}}`, `{{division_name}}`, `{{report_date}}`), `templates/blocks/doctor-note.html` (`{{note-id}}`), `templates/blocks/panel-details.html` (`{{panel-id}}`, `{{panel-title}}`).
17
+
18
+ Note: `lib/sendEmail.js` uses a **different** placeholder convention for its own hand-built HTML — single-brace `{token}` (e.g. `{logo}`, `{header_text}`) — not `{{double-brace}}`. If you're hunting for a placeholder and it's an email string, look for single braces.
19
+
20
+ **(b) jQuery-over-jsdom DOM manipulation**, for the main body of each report. The pattern:
21
+
22
+ 1. Load a page-skeleton HTML file (e.g. `templates/ltr_no_pages.html`) with `fs.readFileSync`.
23
+ 2. `let dom = new JSDOM(html); $ = require('jquery')(dom.window);` — construct a real DOM in Node and bind jQuery to it.
24
+ 3. Load each block fragment, do its `{{token}}` replacements, then inject it into the DOM by selector: `$('#content').append(newPanelHtml)`, `$("#reference").text(data.reportNumber)`, `$('.patient-table').html(...)`, `$(selector).remove()`, etc.
25
+ 4. Serialize the whole document back to a string with `dom.serialize()` and write it to a temp/output HTML file.
26
+ 5. Puppeteer loads that file via a `file://` URL and rasterizes it to PDF (see 3.3).
27
+
28
+ This exact five-step pattern is duplicated **independently** in `lib/pdf_generator.min.js`, `lib/wellbeing_report_generator.js`, `lib/corporate_report_generator.js`, and `lib/sanusx_report_generator.js` — there is no shared "render engine" module between them, only convention.
29
+
30
+ `lib/template.js` is a partial exception: it contains a newer, parallel set of rendering functions (suffixed `V3` — `renderBiomarkerV3`, `renderPanelV3`, `renderNoteV3`, etc.) that build HTML purely via JS template-literal string concatenation, with no `fs.readFileSync` of block `.html` files and no jsdom/jQuery for those specific pieces. `template.js` is used **only** by `wellbeing_report_generator.js` (for rendering the merged "SmartReport" biomarker section) — it is not shared with the core report's own (older, still jsdom/jQuery-based) biomarker rendering in `pdf_generator.min.js`, even though the two do very similar things. This suggests the codebase is mid-migration from "external HTML blocks + jQuery/jsdom" toward "HTML generated inline in JS," but the migration has only reached the wellbeing pipeline so far.
31
+
32
+ ## 3.2 Top-level page templates — live vs. dead
33
+
34
+ Only two root-level templates are actually loaded at runtime by the core report: **`templates/ltr_no_pages.html`** and **`templates/rtl_no_pages.html`**, chosen by a single `isRtl` boolean. Both are minimal skeletons — a `<head>` pulling in `charts.min.js`/`qrcode.min.js`, empty containers (`#patient-container`, `#more-link`, `#pin`, `<canvas id="canvas">`), and one empty `<div id="content">` that everything else gets appended into. The RTL variant additionally sets `dir="rtl" lang="ar"` and loads `arabic.min.css`/`translation.min.js`. Neither contains manual page-break markup — pagination is left entirely to the print engine, which is why they're named `*_no_pages` (as opposed to the older, page-per-`<div class="paper">` style below).
35
+
36
+ `templates/imc/first-header-template.html` is swapped in only for one specific lab type (`labType === 9`, a wide custom header).
37
+
38
+ The remaining root-level files — `templates/base.html`, `templates/template.html`, `templates/ltr.html`, `templates/no_pages.html`, `templates/empty.html`, `templates/first_page_head.html` — are **not referenced by any code path** (confirmed by grep across all of `lib/`). They are static mockups/prototypes from earlier iterations of the pagination model (manual `<div class="paper">` per-page blocks with repeated headers/footers) or trivial smoke-test fixtures. Don't assume editing them affects output — they're dead weight, safe candidates for removal but currently harmless if left alone.
39
+
40
+ ## 3.3 PDF conversion (Puppeteer)
41
+
42
+ Each generator launches its own Puppeteer instance and calls `page.pdf(...)` with its own options — there is no shared "renderPdf" helper across the four pipelines.
43
+
44
+ **Core report** (`pdf_generator.min.js`) calls `page.pdf()` **twice**, because Chrome's print header/footer can't vary within a single call:
45
+ - Once for `pageRanges: '1'` with the "first page" header template and different top margin (page 1 needs the patient-info/QR header).
46
+ - Once for `pageRanges: '2-'` with the regular header template.
47
+
48
+ The two resulting PDF buffers are then merged into one file using `hummus` (`createWriterToModify(...).appendPDFPagesFromPDF(...)`).
49
+
50
+ **Wellbeing / Corporate / SanusX** each call `page.pdf()` once, with their own `headerTemplate`/`footerTemplate`/margins tuned per report type. The SanusX generator additionally passes `pageRanges: '1'`, meaning **any content overflowing page 1 is silently dropped** — a fragile single-page assumption worth remembering if SanusX report content ever grows (e.g. longer translated strings pushing content past one page).
51
+
52
+ The corporate report generator sets page content **twice** — once via `page.setContent(...)` and again by navigating to the serialized temp HTML file via `page.goto("file://...")` — and separately injects jQuery from a public CDN (`cdn.jsdelivr.net`) mid-render via `page.addScriptTag`. This means corporate-report rendering has a live external-network dependency at PDF-generation time, unlike the other three pipelines which bundle their own JS/CSS as local `assets/` files.
53
+
54
+ ## 3.4 Charts
55
+
56
+ There is no server-side chart *image* generation. The npm `chart.js` package is `require`d at the top of `pdf_generator.js`/`.min.js` but never actually called — a dead import. Instead:
57
+
58
+ 1. Server-side, biomarker history is serialized into a plain JS array (`{id, dataset, dataColor, dataLabel}` per biomarker) and injected into a `<script>` tag as `var chartsData = [...]`.
59
+ 2. The page template loads a **separately bundled, minified browser copy of Chart.js 2.x** — `assets/charts.min.js` (not the npm package).
60
+ 3. `assets/js.js`'s `drowBioCharts()` function runs **inside the headless Chrome page itself**, before `page.pdf()` is called, and instantiates real `Chart(ctx, {type:'line', ...})` objects against `<canvas>` elements.
61
+
62
+ So charts are rendered client-side, in-browser, immediately before the PDF snapshot — a legitimate technique, but it means the npm `chart.js` dependency and `assets/charts.min.js` are two independent copies of the same library that can silently drift out of version sync.
63
+
64
+ The corporate report's bar charts don't use a charting library at all: `renderBarChart()` emits plain `<div class="bar-chart" data-value=".." data-total="..">` elements, and `assets/corporate_report/js/js.js` computes their pixel width client-side via `$(this).css("width", "calc(" + percent + "% + 60px)")`. SanusX's score "gauges" are similarly library-free — static PNG segment-circle images overlaid with a CSS-rotated needle (`transform: rotateZ(...)`), not a canvas or SVG chart.
65
+
66
+ ## 3.5 QR codes — two unrelated mechanisms
67
+
68
+ Don't confuse these:
69
+
70
+ 1. **The in-report patient PIN/QR box** (page 1 "more info" panel): the server builds a plain concatenated string (`'v_' + patientName + '_!_' + pin`, misleadingly assembled near variables named `base64text`/`base64name`/`base64pin` that are computed but never actually used), injects it as a script variable, and `assets/js.js`'s `drawQRCode()` calls `QRCode.toCanvas(...)` **client-side, in-browser**, using the bundled `assets/qrcode.min.js`, targeting a `<canvas id="canvas">` in the page template.
71
+ 2. **`generatePatientQR(data)`** (exported from `index.js` as `generateQrCode`) — despite its name, this function does **not generate a QR code**. It takes an **already-fully-rendered HTML string** from the caller (e.g. containing an inline `<svg>` QR code the caller produced elsewhere), wraps it in jsdom, writes it to a temp file, and rasterizes it to a `letter`-format PDF with no margins/headers via Puppeteer. It is a generic "HTML string → PDF" utility that happens to be used for QR codes by convention, not a QR-code generator itself. See `tests/test-qr-code.js` for the expected caller-side pattern (pre-render the QR as inline SVG, then pass the whole HTML string in).
@@ -0,0 +1,128 @@
1
+ # 4. Report Pipelines
2
+
3
+ Four independent report types. Each has its own generator module, its own template folder(s), and its own quirks.
4
+
5
+ ---
6
+
7
+ ## 4.A Core "Medicus" Lab Report
8
+
9
+ **Module:** `lib/pdf_generator.min.js` (the file that actually runs — see [doc 1.4](01-architecture-overview.md#14-critical-fact-the-core-report-renderer-that-actually-runs-is-pdf_generatorminjs-not-pdf_generatorjs)). `lib/pdf_generator.js` is a stale reference copy.
10
+
11
+ **Purpose:** a clinical lab report — biomarkers grouped into panels, each with reference ranges, history charts, doctor notes, and AI/clinical "insights." No questionnaire concept at all.
12
+
13
+ **Entry points:** `generateHTMLStaging(data, isDebugging)` → HTML; `generatePDF(html)` → PDF buffer via Puppeteer; both wired together by `index.js`'s `generateMedicusPDF`. `generatePatientQR(data)` is a separate, generically-named "HTML string → PDF" utility (see [doc 3.5](03-templating-and-rendering.md#35-qr-codes--two-unrelated-mechanisms)).
14
+
15
+ **Data shape (top-level fields observed in the code and `assets/data/data2.json`):**
16
+ ```
17
+ language, labType (9 = "IMC lab", triggers customHeader), labLogo, labName, labAddress,
18
+ labPhoneNumber, patientName, patientPin, showPIN, timeZoneOffset, reportDate (unix seconds),
19
+ reportNumber, reportDivision, reportTitle, copyright, moreLink, showCompactView, showDetailsView,
20
+ profileItems: [{title, value}],
21
+ panels: [{ panelId, name, panelDetails, notes,
22
+ biomarkers: [{ id, name, fullName, unit, value, formattedValue, color, isNormal,
23
+ showInCompactView, showInDetailsView, ranges[], history[],
24
+ childrenBiomarkers[], doctorNotes[], relatedInsights[],
25
+ status, insightsCount }],
26
+ insights: [...] }],
27
+ summary: [{ ...stat insights, stackedInsight, relatedBiomarkers, isClinicalReading }],
28
+ doctorNote: [{ id, createdByName, content }],
29
+ reportSummary, reportProperties,
30
+ signatures: [{ name, signatureURL }], labStamp, approvalDate
31
+ ```
32
+
33
+ **Templates:** always `templates/blocks/*.html` (shared, non-brand) plus `templates/imc/first-header-template.html` for `labType === 9`. No per-brand template folder is ever consulted in this pipeline — whitelabelling here happens entirely through data values the host app bakes into the payload (logo URL, colors would need to be pre-applied since there's no `config/` lookup at all in this file).
34
+
35
+ **Notable quirks:**
36
+ - Module-level mutable state (`$`, `debug`, `OUT_FILE`, `Pdf_file`, `shouldRenderCustomHeader`) is reassigned per call with no isolation — concurrent calls in the same process can interfere with each other (contrast with `generateFullPdf`'s UUID-based temp files).
37
+ - Every render leaves a timestamped HTML file behind in the package's own `output/` directory; nothing is cleaned up.
38
+ - `labType === 9` ("IMC") special-casing runs throughout what's otherwise framed as the generic/core pipeline.
39
+ - Pinned to very old `puppeteer@^1.15.0` and `hummus@1.0.111`.
40
+
41
+ ---
42
+
43
+ ## 4.B Wellbeing Report (+ optional merged "SmartReport")
44
+
45
+ **Module:** `lib/wellbeing_report_generator.js` (2000+ lines), using shared rendering helpers from `lib/template.js` for the SmartReport section, and `lib/big_integral_questionnaire.js` for one Maison-Sante-only static table.
46
+
47
+ **Purpose:** a patient-facing wellbeing/lifestyle report — physical/psychological/lifestyle scores, PHQ‑9/GAD‑7-style questionnaire answers (when a doctor reviewed it), and tips. Optionally merged with a full lab biomarker report ("SmartReport").
48
+
49
+ There are **two distinct entry functions** with materially different behavior:
50
+
51
+ **`generateHTMLWellbeingReport`** (the plain flow):
52
+ - Signature: `(data, isDebugging, clientName, language)`
53
+ - Template folder: always `templates/wellbeing/` (generic), regardless of client
54
+ - Data source: top-level `data.calculation` / `data.partsScore` / `data.insights.tips`
55
+ - Called from: `generateNascoPDF` (`index.js`)
56
+ - PDF conversion: `generatePDFWellbeingReport(html)`
57
+
58
+ **`generateHTMLWellbeingReportWithSmartReport`** (the branded/extended flow):
59
+ - Signature: `(data, isDebugging, clientName, language, selectedLabs=[], showHeaderLogo=true, hideWellbeingUI=false)`
60
+ - Template folder: `templates/{Mediclinic, Pha, or maisonsante}/` — client-specific, throws if the client has no dedicated folder
61
+ - Data source: `data.wellbeing` (scores), `data.profileInfo` (metadata), `data.Profile` + `data.IsDoctor` (Q&A), `data.SR` (merged SmartReport lab data)
62
+ - Called from: `generateFullPdf` (`index.js`) — the only flow that also encrypts the output
63
+ - PDF conversion: `generatePDFReport(data, hideWellbeingUI)` — adds running header/footer, `data.footer` support
64
+
65
+ **Brand/theme support:** `loadColorTheme` explicitly supports **mediclinic, pha, nasco, maisonsante**. Any other client string (`bionext`, `diagnostikare`, `sanitas`, `sanusx`, `Najeeb.ai`) silently falls back to the mediclinic color theme.
66
+
67
+ **SmartReport merge (`data.SR`):** a full lab/biomarker report payload (same general shape as the corporate report's item list: `items[]` of `biomarker`, `panel`, `insight`, `biomarkerNote`, `allergyItems`, etc.), parsed as string or object, run through `generateReportHtml()` (reusing `renderReportItems`/`renderSummary`/`renderHTMLTemplate` from `lib/template.js`), and injected into `#smart-report`. If `hideWellbeingUI` is true or `data.wellbeing` is empty, the quiz-UI sections are removed from the DOM so only the lab report shows.
68
+
69
+ **`selectedLabs` / `showHeaderLogo`:** used to decide which lab logos appear in the header, normalized against `config.labs_logo`/`labs_logo_aliases` (Pha only — see [doc 2.1](02-configuration-and-whitelabelling.md#211-the-nested-corporate_report-object)) so different spellings of a lab name (`"lab corp"`, `"LabCorp"`) resolve to one config entry.
70
+
71
+ **Known bug:** `index.js` passes `showHeaderLogo || true` into the generator. Since `false || true === true`, **the header logo can never actually be suppressed**, contradicting both the parameter name and its JSDoc in `index.js`.
72
+
73
+ **Localization:** uses `app/i18n_wellbeing.config.js` / `locales/wellbeing/*.json` (not the main `locales/` set — see [doc 6](06-internationalization.md)).
74
+
75
+ **Encryption:** only `generateFullPdf` in `index.js` encrypts the final buffer, using `node-qpdf2` with `reportData.pdfPassword`. `wellbeing_report_generator.js` itself has no encryption logic — it only ever returns a plaintext PDF buffer.
76
+
77
+ **Other quirks worth knowing:**
78
+ - Per-client special-casing is scattered as inline conditionals rather than being config-driven: PHA-only CSS overrides, PHA-only bottom margin, hardcoded per-client addresses/phone numbers in the footer builder, tips category ordering swapped for `pha`, header-logo sizing branched by client name. Onboarding a new client for this flow means hunting through many such `if (client === ...)` branches, not editing one config file.
79
+ - `generateHeaderInfo()`'s output is computed but discarded — its target selector `.report-info` is commented out in all three brand `blocks/header.html` files. The live equivalent is `renderPatientTable()`, which injects into `.patient-table`.
80
+ - `templates/{wellbeing,Mediclinic,Pha,maisonsante}/wellbeing_template.html` files are not referenced by any `.js` file — orphaned, superseded by `ltr_no_pages.html`/`rtl_no_pages.html`.
81
+ - Shared mutable module state (`config`, the singleton `localeService`/`i18n`, a module-level `debug` flag) is reassigned per call; file paths were hardened against concurrent-request collisions (per an in-code comment referencing a real past bug) but config/locale/debug state was not similarly isolated — concurrent requests for different clients/languages can still race.
82
+
83
+ ### 4.B.1 `lib/big_integral_questionnaire.js` — read this before assuming it's a real feature
84
+
85
+ This module is **not** a scoring engine and does **not** read real questionnaire answers from the input payload. It is a presentational renderer for one static demo table ("Key to the Big Integral Questionnaire" — a functional-medicine systems review, unrelated to the PHQ‑9/GAD‑7 wellbeing scores) built entirely from `getDummyData()`, which returns 11 hardcoded rows with `result: 0` for every row and a fixed fake patient (`"Final Test 3"`, age 37). The single export, `renderBigIntegralQuestionnaire({ clientConfig })`, merges `clientConfig.big_integral_questionnaire` colors/toggles (from `config/maisonsante.json`) and renders the table. It is invoked only when `client.toLowerCase() === 'maisonsante' && data.IsDoctor`, targeting `#big-integral-section` (present only in `templates/maisonsante/ltr_no_pages.html`). `preview-big-integral.js` is a standalone dev harness confirming this is a design/preview scaffold, not a wired, data-driven feature. **If a stakeholder asks "why doesn't the Big Integral Questionnaire reflect the patient's real answers" — that's not a bug, it was never wired to real data.**
86
+
87
+ ---
88
+
89
+ ## 4.C Corporate Report
90
+
91
+ **Module:** `lib/corporate_report_generator.js`.
92
+
93
+ **Purpose:** an aggregate/analytics PDF summarizing results across a *population* of participants (e.g. an employer's workforce), not a single patient — cover page, "Participant Analytics" section (per-metric bar charts), then one page per health "element" with a summary, bar charts, textual "relations," a gender-breakdown table, recommendations, and sources. Built for HR/benefits stakeholders.
94
+
95
+ **Entry points:** `generateHTMLCorporateReport(data, isDebugging)` → HTML; `generatePDFCorporateReport(html)` → PDF; wired together by `index.js`'s `generateCorporateReportPDF(json, isDebugging, isDownloadable)`.
96
+
97
+ **Data shape:** `data.clientName` (branding selector), `data.language`, `data.participantAnalytics: [...]` (drives the Participant Analytics bar charts — the `participant_analytics.html` template itself is just an empty `<div class="container">`; all its content is generated in JS), `data.elements: [{ summary, chartValues, secondChartValues, relations, dataByGender, recommends, sources }]`.
98
+
99
+ **Templates:** always `templates/corporate_report/*` (`cover_page.html`, `participant_analytics.html`, `page.html`, `footer.html`), regardless of client — only colors/logo vary by `config/{client}.corporate_report`.
100
+
101
+ **RTL:** this generator correctly branches to `templates/corporate_report/rtl_no_pages.html` vs `ltr_no_pages.html` based on `data.language` containing `"ar"` — unlike SanusX (4.D), RTL is not broken here.
102
+
103
+ **Notable quirks:**
104
+ - Sets page content twice (`page.setContent` then `page.goto(file://...)`) and injects jQuery from a public CDN mid-render (`page.addScriptTag({ url: 'https://cdn.jsdelivr.net/...' })`) — a live external-network dependency at render time.
105
+ - Contains a copy-pasted, entirely dead `combinePDFBuffers` function (and the `hummus`/`memory-streams` requires that exist only to support it) — never actually invoked in this file.
106
+ - Onboarding a new client's custom cover-page background image requires editing shared CSS by client-name string (`.cover-overlay-container.bionext { background-image: url(...) }` in `assets/corporate_report/css/report.css`) — bypasses the otherwise config-JSON-driven theming model.
107
+ - Corporate-report translation strings live inside the `locales/wellbeing/` bucket (via `app/i18n_wellbeing.config.js`), not a dedicated namespace — a naming trap when hunting for a string to translate.
108
+
109
+ ---
110
+
111
+ ## 4.D SanusX Report
112
+
113
+ **Module:** `lib/sanusx_report_generator.js`.
114
+
115
+ **Purpose:** a branded, single-consumer wellbeing product ("Your Health Hero") — an individual is scored on physical/psychological axes and assigned an avatar archetype (monkey/tiger/owl) with a "superpower," a strength/weakness, three predicted-vs-actual insight cards, and three tips cards. The SanusX-brand analogue of the wellbeing report.
116
+
117
+ **Entry points:** `generateHTMLSanusXReport(data, isDebugging, clientName, language)` → HTML; `generateSanusXReport(html)` → PDF; wired together by `index.js`'s `generateSanuxPDF`.
118
+
119
+ **Data shape:** `data.highestAvatarScorePartId` (1/2/3, maps to monkey/tiger/owl), score fields feeding a rounded physical/psychological score bucketed into low/med/high (drives a static PNG "speedometer" image + CSS-rotated needle, not a real chart), `data.PartScoresHighestValues.{body,mind,lifeStyle}[0].isPredictiveElement` (drives "predicted right/wrong" copy), `data.insights.tips` (3 items, split into lifestyle/body/mind cards).
120
+
121
+ **Templates:** always `templates/sanusx/*`, regardless of `clientName`.
122
+
123
+ **Notable quirks — more fragile than the other three pipelines:**
124
+ - **Ignores `clientName` for branding entirely.** Config is hardcoded to `require('../config/sanusx.json')`. The `client` parameter is only used to add a CSS class (`$(".score-main").addClass(client)`) that currently matches no rule in `assets/sanusx/css/sanusx_report.css` — a no-op.
125
+ - **RTL is broken/incomplete.** There's a comment (`/*check if the language is an RTL language*/`) with no logic after it — `templates/sanusx/ltr_no_pages.html` is loaded unconditionally regardless of `language`. `templates/sanusx/rtl_no_pages.html` exists on disk but references asset paths from the *wellbeing* report (`assets/wellbeing/css/nasco_report.css`) that don't exist under `assets/sanusx/` — it is dead, unreachable code, not a working alternative.
126
+ - **`page.pdf({ pageRanges: '1', ... })` hard-limits output to one page** — any overflow (e.g. from longer translated strings) is silently dropped rather than flowing to a second page.
127
+ - `templates/sanusx/blocks/tips.html` exists on disk but is never read by this generator (dead file) — the actual tips rendering is done by an in-file `renderInsightTips()` function building HTML via string concatenation.
128
+ - Reuses `sendNascoEmail` (named after a different client) for its email flow, same as the wellbeing and corporate pipelines — a shared helper with a client-specific name baked into shared infrastructure.
@@ -0,0 +1,67 @@
1
+ # 5. Questions, Answers & the Data Model
2
+
3
+ This section directly answers the question: **"How do we render a question, and where does the question text come from?"**
4
+
5
+ ## 5.1 The headline fact: there is no question bank in this codebase
6
+
7
+ This package does not store, own, or look up questionnaire questions. There is no database, no question-ID → question-text mapping, no survey-definition file anywhere in the repository. **Every question's title text and its answer value arrive together, pre-rendered, as flat `{title, value}` pairs inside the JSON payload the host application sends in.** This package's job is purely to lay that text out on the page — not to know what the questions mean, score them, or validate them.
8
+
9
+ This matters operationally: if a question's wording is wrong in a PDF, the fix is almost never in this repository — it's in whatever upstream system assembled the `Profile`/`profileInfo` array before base64-encoding it and calling this library.
10
+
11
+ ## 5.2 Where question/answer data lives, per pipeline
12
+
13
+ ### Core "Medicus" lab report — no questions at all
14
+
15
+ This pipeline's domain model is biomarkers/panels/insights/doctor-notes/signatures (see [doc 4.A](04-report-pipelines.md#4a-core-medicus-lab-report)). Grepping the entire core renderer for "question" or "answer" returns nothing. If you need to document or debug question rendering, **you are in the wrong pipeline** — go to the wellbeing report instead.
16
+
17
+ ### Wellbeing report — `data.Profile` (the actual Q&A) and `data.profileInfo` (metadata)
18
+
19
+ Two separate arrays, both flat `{title, value}` lists, both only meaningfully populated when `data.IsDoctor === true`:
20
+
21
+ ```js
22
+ // testing-reports/mediclinic/doctor-data.js — a real fixture used by test.js
23
+ {
24
+ "IsDoctor": true,
25
+ "Profile": [
26
+ { "title": "What is your gender?", "value": "Male" },
27
+ { "title": "When is your birthday?", "value": "51 years" },
28
+ { "title": "What is your height?", "value": "178 cm" },
29
+ { "title": "Do you have any of the following conditions?", "value": "Asthma" },
30
+ { "title": "Do you have a family history of any condition?",
31
+ "value": "Diabetes: No one | High blood pressure: No one" },
32
+ { "title": "Over the last 2 weeks, how often have you been bothered by feeling down, depressed, or hopeless?",
33
+ "value": "More than half the days" },
34
+ // ... a full PHQ-9 / GAD-7 style clinical questionnaire, plus lifestyle questions
35
+ ]
36
+ }
37
+ ```
38
+
39
+ - **`data.Profile`** — the real questionnaire content (PHQ‑9/GAD‑7-style clinical questions plus lifestyle questions). Rendered by `renderDoctorDetails(doctorData)` in `lib/wellbeing_report_generator.js`: each item becomes a two-column row — `.question-title` for `item.title`, `.details-value` for `item.value` — under a "Quiz Answers" header, injected into `.doctor-details` in the page DOM. **Neither the question title nor the answer value is HTML-escaped here** — see the security note in 5.4.
40
+ - **`data.profileInfo`** — patient/report metadata (LAB, Reference, Patient name, Report date, Date of birth, Weight, Height, Gender, BMI, Blood Pressure, Waist Circumference), *not* questionnaire answers. Split by array index: the first 4 entries go through `generateHeaderInfo()` (whose output is actually **dead** — its target `.report-info` is commented out of every brand's `header.html`), the rest go through `renderPatientTable()`, which *does* escape the label (not the value) and is injected live into `.patient-table`.
41
+ - Numeric wellbeing **scores** (as opposed to raw Q&A text) come from a different part of the payload — `data.wellbeing.calculation` / `data.wellbeing.partsScore` — pre-computed upstream; this package does not calculate PHQ‑9/GAD‑7 scores itself, it only displays whatever score value it's handed.
42
+
43
+ ### `lib/big_integral_questionnaire.js` — a cautionary example, not a real data source
44
+
45
+ As covered in [doc 4.B.1](04-report-pipelines.md#4b1-libbig_integral_questionnairejs--read-this-before-assuming-its-a-real-feature), this module currently renders **100% hardcoded dummy data** (`getDummyData()` — 11 fixed rows, `result: 0` for all of them, a fake patient name). It is not wired to any real answer data in the payload today. Do not use it as a reference for "how question rendering should work" — use `renderDoctorDetails`/`data.Profile` instead.
46
+
47
+ ### Corporate report and SanusX — no free-text Q&A
48
+
49
+ Neither of these two pipelines renders question/answer pairs in the `data.Profile` sense. The corporate report works with pre-aggregated `data.elements[]` (population-level summaries, chart values, gender breakdowns — see [doc 4.C](04-report-pipelines.md#4c-corporate-report)). SanusX works with pre-computed scores and flags (`highestAvatarScorePartId`, `isPredictiveElement` — see [doc 4.D](04-report-pipelines.md#4d-sanusx-report)). Both assume all interpretation/scoring already happened upstream.
50
+
51
+ ### "SmartReport" biomarker notes — a different kind of "note," not a question
52
+
53
+ When a wellbeing report is merged with lab data (`data.SR`, see [doc 4.B](04-report-pipelines.md#4b-wellbeing-report--optional-merged-smartreport)), individual biomarkers can carry `doctorNote`/`biomarkerNote`/`insight` entries. These are clinician-authored free-text notes attached to a specific lab value — a different concept from a questionnaire answer — rendered via `renderReportItems`/`renderNoteV3` in `lib/template.js`, not via `renderDoctorDetails`.
54
+
55
+ ## 5.3 Summary
56
+
57
+ - **`data.Profile`** (Wellbeing) — real questionnaire Q&A (title + value pairs). Rendered by `renderDoctorDetails` → `.doctor-details`. **Not** HTML-escaped.
58
+ - **`data.profileInfo`** (Wellbeing) — patient/report metadata, not Q&A. Rendered by `renderPatientTable` (live) / `generateHeaderInfo` (dead code). Label is escaped; value is not.
59
+ - **`data.wellbeing.calculation` / `.partsScore`** (Wellbeing) — pre-computed numeric scores. Rendered as score bars/gauges. N/A for escaping (numeric).
60
+ - **`big_integral_questionnaire.js` internal `getDummyData()`** (Wellbeing, Maison Sante only) — hardcoded demo rows, **not real answers**. Rendered by `renderTable`.
61
+ - **`data.SR.items[].biomarkerNote` / `.doctorNote` / `.insight`** (Wellbeing, SmartReport merge) — clinician notes on a lab value. Rendered by `renderReportItems`/`renderNoteV3` (`lib/template.js`). Escaping varies.
62
+ - **`data.elements[]`** (Corporate) — pre-aggregated population analytics. Rendered by `renderBarChart` and table builders.
63
+ - **`data.PartScoresHighestValues`, `data.insights.tips`** (SanusX) — pre-computed scores/flags. Rendered by the avatar/predictions/tips sections.
64
+
65
+ ## 5.4 Security note: unescaped answer text
66
+
67
+ `renderDoctorDetails` (wellbeing `data.Profile`) injects `item.value` — and, in one further case, `item.title` — as raw HTML with no escaping. If any upstream system ever allows free-text answers containing HTML/script-like content to flow into this field unsanitized, that content will be injected verbatim into the rendered page before Puppeteer snapshots it to PDF. Because report generation happens server-side inside a headless Chrome instance (not a browser the end user controls), this is not a classic reflected-XSS-in-the-browser risk to the *end user*, but it is a real risk if payload data can come from a less-trusted source than the doctor/clinician — e.g. if patient-entered free text were ever passed straight through into `Profile[].value` without sanitization upstream. If you're auditing security boundaries, treat "who is allowed to populate `data.Profile`, and is it sanitized before it reaches this library" as an open question to raise with the host application team — this library does not sanitize it.