@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
@@ -1,298 +1,370 @@
1
- "use strict";
2
-
3
- // ---------------------------------------------------------------------------
4
- // Dummy data — structure mirrors the Excel RESULTS KEY sheet.
5
- // result = 0 for all rows (questionnaire not yet filled in).
6
- // Hormonal Health rows have no High/Questions/Maximum — rendered as "—".
7
- // ---------------------------------------------------------------------------
8
- function getDummyData() {
9
- return {
10
- rows: [
11
- {
12
- system: "STOMACH and GI (Gastrointestinal) Tract",
13
- high: ">8",
14
- questions: 9,
15
- result: 0,
16
- maximum: 27,
17
- },
18
- {
19
- system: "Small Intestine",
20
- high: ">7",
21
- questions: 8,
22
- result: 0,
23
- maximum: 24,
24
- },
25
- {
26
- system: "Infections/Invasions of the GI (Gastrointestinal Tract)",
27
- high: ">3",
28
- questions: 4,
29
- result: 0,
30
- maximum: 12,
31
- },
32
- {
33
- system: "Large Intestine",
34
- high: ">2",
35
- questions: 3,
36
- result: 0,
37
- maximum: 9,
38
- },
39
- {
40
- system: "Liver and Gallbladder",
41
- high: ">7",
42
- questions: 8,
43
- result: 0,
44
- maximum: 22,
45
- },
46
- {
47
- system: "Essential Nutrients",
48
- high: ">25",
49
- questions: 26,
50
- result: 0,
51
- maximum: 68,
52
- },
53
- {
54
- system: "Carbohydrate Metabolism",
55
- high: ">4",
56
- questions: 4,
57
- result: 0,
58
- maximum: 12,
59
- },
60
- {
61
- system: "Immune System",
62
- high: ">3",
63
- questions: 4,
64
- result: 0,
65
- maximum: 10,
66
- },
67
- { system: "Lifestyle", high: ">7", questions: 7, result: 0, maximum: 7 },
68
- {
69
- system: "Hormonal Health Female",
70
- high: null,
71
- questions: null,
72
- result: 0,
73
- maximum: null,
74
- },
75
- {
76
- system: "Hormonal Health Male",
77
- high: null,
78
- questions: null,
79
- result: 0,
80
- maximum: null,
81
- },
82
- ],
83
- meta: {
84
- patientName: "Final Test 3",
85
- age: "37",
86
- bmi: "23.67",
87
- },
88
- };
89
- }
90
-
91
- // ---------------------------------------------------------------------------
92
- // Helpers
93
- // ---------------------------------------------------------------------------
94
- function fmt(val, decimals) {
95
- if (val === null || val === undefined) return "\u2014"; // em dash
96
- if (typeof val === "number") {
97
- const rounded =
98
- Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals);
99
- return String(rounded);
100
- }
101
- return String(val);
102
- }
103
-
104
- // ---------------------------------------------------------------------------
105
- // Single render — faithful to the reference image, Maison Sante colours.
106
- //
107
- // Structure:
108
- // ┌──────────────────────────────────────────────────────┐
109
- // │ Key to the Big Integral Questionnaire [title bar] │
110
- // ├──────────────────────────────────────────────────────┤
111
- // │ Client/Patient Full Name … Age … BMI … │
112
- // ├──────────────────────────────────────────────────────┤
113
- // │ SYSTEM SCORES [subheader band] │
114
- // ├────────────┬──────┬─────────┬────────┬───────┬──┬───┤
115
- // │ SYSTEM │ High │Questions│ Result │Maximum│/3 │Cmt│
116
- // ├────────────┼──────┼─────────┼────────┼───────┼──┼───┤
117
- // │ ...rows... │
118
- // └──────────────────────────────────────────────────────┘
119
- // ---------------------------------------------------------------------------
120
- function renderTable(rows, meta, cfg) {
121
- const c = cfg.colors;
122
- const title = cfg.title || "Key to the Big Integral Questionnaire";
123
-
124
- // ── patient info bar ──────────────────────────────────────────────────
125
- const infoBar = `
126
- <div style="background:#ffffff;border-bottom:1px solid ${c.rowBorder};">
127
- <table style="border-collapse:collapse;">
128
- <tr>
129
- <td style="padding:7px 40px 7px 14px;font-size:9px;color:#1a1a1a;white-space:nowrap;vertical-align:middle;">
130
- <span style="color:${c.headerBg};font-weight:700;">Client/Patient Full Name</span>
131
- &nbsp;&nbsp;&nbsp;${meta.patientName}
132
- </td>
133
- <td style="padding:7px 40px 7px 0;font-size:9px;color:#1a1a1a;white-space:nowrap;vertical-align:middle;">
134
- <span style="color:${c.headerBg};font-weight:700;">Age</span>
135
- &nbsp;&nbsp;&nbsp;${meta.age}
136
- </td>
137
- <td style="padding:7px 0 7px 0;font-size:9px;color:#1a1a1a;white-space:nowrap;vertical-align:middle;">
138
- <span style="color:${c.headerBg};font-weight:700;">BMI</span>
139
- &nbsp;&nbsp;&nbsp;${meta.bmi}
140
- </td>
141
- </tr>
142
- </table>
143
- </div>`;
144
-
145
- // ── "SYSTEM SCORES" subheader band ───────────────────────────────────
146
- const subheaderBg = c.subheaderBg || "#1B4080";
147
- const subheaderBand = `
148
- <div style="
149
- padding: 5px 14px;
150
- background: ${subheaderBg};
151
- color: #ffffff;
152
- font-size: 8.5px;
153
- font-weight: 700;
154
- letter-spacing: 0.8px;
155
- text-transform: uppercase;
156
- ">SYSTEM SCORES</div>`;
157
-
158
- // ── column widths ──────────────────────────────────────────────────────
159
- const COL = {
160
- system: "width:34%;text-align:left;",
161
- high: "width:8%; text-align:center;",
162
- questions: "width:9%; text-align:center;",
163
- result: "width:8%; text-align:center;",
164
- maximum: "width:9%; text-align:center;",
165
- perThree: "width:8%; text-align:center;",
166
- comments: "width:24%;text-align:left;",
167
- };
168
-
169
- const TH_BASE = `padding:6px 14px;font-size:8px;font-weight:700;color:#ffffff;
170
- background:${c.headerBg};border-bottom:2px solid rgba(255,255,255,0.2);`;
171
-
172
- const th = (label, colKey) =>
173
- `<th style="${TH_BASE}${COL[colKey]}">${label}</th>`;
174
-
175
- const tableHeader = `
176
- <tr>
177
- ${th("SYSTEM", "system")}
178
- ${th("High", "high")}
179
- ${th("Questions", "questions")}
180
- ${th("Result", "result")}
181
- ${th("Maximum", "maximum")}
182
- ${th("/3", "perThree")}
183
- ${th("Comments", "comments")}
184
- </tr>`;
185
-
186
- // ── data rows ─────────────────────────────────────────────────────────
187
- const TD = (content, colKey, extra = "") =>
188
- `<td style="padding:5px 14px;font-size:8.5px;${COL[colKey]}border-bottom:1px solid ${c.rowBorder};${extra}">${content}</td>`;
189
-
190
- const dataRows = rows
191
- .map((row, i) => {
192
- const hasThreshold = row.high !== null && row.high !== undefined;
193
- const hasQuantity = row.questions !== null && row.questions !== undefined;
194
- const hasMaximum = row.maximum !== null && row.maximum !== undefined;
195
-
196
- const highDisplay = hasThreshold
197
- ? `<span style="color:${c.elevatedText};font-weight:600;">${row.high}</span>`
198
- : "\u2014";
199
-
200
- const questionsDisplay = hasQuantity ? String(row.questions) : "\u2014";
201
- const resultDisplay = String(row.result);
202
- const maximumDisplay = hasMaximum ? fmt(row.maximum, 0) : "\u2014";
203
- const perThreeDisplay = hasMaximum ? fmt(row.maximum / 3, 2) : "\u2014";
204
-
205
- const rowBg = i % 2 === 0 ? "#ffffff" : "rgba(15, 32, 98, 0.03)";
206
-
207
- return `
208
- <tr style="background:${rowBg};">
209
- ${TD(`<span style="color:#1a1a1a;">${row.system}</span>`, "system")}
210
- ${TD(highDisplay, "high")}
211
- ${TD(questionsDisplay, "questions")}
212
- ${TD(resultDisplay, "result", `font-weight:600;color:#444;`)}
213
- ${TD(maximumDisplay, "maximum")}
214
- ${TD(perThreeDisplay, "perThree")}
215
- ${TD("", "comments")}
216
- </tr>`;
217
- })
218
- .join("");
219
-
220
- // ── assemble ──────────────────────────────────────────────────────────
221
- // Outer wrapper uses padding-top instead of an empty spacer div, because
222
- // empty divs with only `height` get collapsed by wkhtmltopdf. Padding
223
- // always renders.
224
- return `
225
- <div style="padding-top:25px;font-family:'Open Sans',sans-serif;">
226
- <div class="biq-wrapper" style="
227
- font-family: 'Open Sans', sans-serif;
228
- page-break-inside: avoid;
229
- border-radius: 4px;
230
- overflow: hidden;
231
- border: 1px solid ${c.rowBorder};
232
- ">
233
- <!-- title bar -->
234
- <div style="
235
- background: ${c.headerBg};
236
- border-radius: 4px 4px 0 0;
237
- color: #ffffff;
238
- padding: 9px 14px;
239
- font-size: 11px;
240
- font-weight: 700;
241
- letter-spacing: 0.3px;
242
- ">${title}</div>
243
-
244
- ${infoBar}
245
- ${subheaderBand}
246
-
247
- <table style="width:100%;border-collapse:collapse;table-layout:fixed;">
248
- <thead>${tableHeader}</thead>
249
- <tbody>${dataRows}</tbody>
250
- </table>
251
- </div>
252
- </div>`;
253
- }
254
-
255
- // ---------------------------------------------------------------------------
256
- // Public entry
257
- // ---------------------------------------------------------------------------
258
- /**
259
- * Render the Big Integral Questionnaire section for Maison Sante doctor PDFs.
260
- *
261
- * @param {object} opts
262
- * @param {object} opts.clientConfig - The full maisonsante.json config object
263
- * @param {object} opts.localeService - localeService with .t(key) method (unused for now; strings are hardcoded to match the reference design)
264
- * @returns {string} HTML string to inject into #big-integral-section
265
- */
266
- function renderBigIntegralQuestionnaire({ clientConfig }) {
267
- const cfg = (clientConfig && clientConfig.big_integral_questionnaire) || {};
268
-
269
- const colors = Object.assign(
270
- {
271
- headerBg: "#0F2062",
272
- subheaderBg: "#1B4080",
273
- headerText: "#FFFFFF",
274
- rowBorder: "rgba(15, 32, 98, 0.12)",
275
- elevatedText: "#A43B2F",
276
- },
277
- cfg.colors || {},
278
- );
279
-
280
- const resolvedCfg = Object.assign({}, cfg, { colors });
281
-
282
- const { rows: allRows, meta } = getDummyData();
283
-
284
- const rows = allRows.filter((row) => {
285
- if (
286
- row.system === "Hormonal Health Female" &&
287
- cfg.showHormonalFemale === false
288
- )
289
- return false;
290
- if (row.system === "Hormonal Health Male" && cfg.showHormonalMale === false)
291
- return false;
292
- return true;
293
- });
294
-
295
- return renderTable(rows, meta, resolvedCfg);
296
- }
297
-
298
- module.exports = { renderBigIntegralQuestionnaire };
1
+ "use strict";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Dummy data — structure mirrors the Excel RESULTS KEY sheet.
5
+ // result = 0 for all rows (questionnaire not yet filled in).
6
+ // Hormonal Health rows have no High/Questions/Maximum — rendered as "—".
7
+ // ---------------------------------------------------------------------------
8
+ function getDummyData() {
9
+ return {
10
+ rows: [
11
+ {
12
+ system: "STOMACH and GI (Gastrointestinal) Tract",
13
+ high: ">8",
14
+ questions: 9,
15
+ result: 0,
16
+ maximum: 27,
17
+ },
18
+ {
19
+ system: "Small Intestine",
20
+ high: ">7",
21
+ questions: 8,
22
+ result: 0,
23
+ maximum: 24,
24
+ },
25
+ {
26
+ system: "Infections/Invasions of the GI (Gastrointestinal Tract)",
27
+ high: ">3",
28
+ questions: 4,
29
+ result: 0,
30
+ maximum: 12,
31
+ },
32
+ {
33
+ system: "Large Intestine",
34
+ high: ">2",
35
+ questions: 3,
36
+ result: 0,
37
+ maximum: 9,
38
+ },
39
+ {
40
+ system: "Liver and Gallbladder",
41
+ high: ">7",
42
+ questions: 8,
43
+ result: 0,
44
+ maximum: 22,
45
+ },
46
+ {
47
+ system: "Essential Nutrients",
48
+ high: ">25",
49
+ questions: 26,
50
+ result: 0,
51
+ maximum: 68,
52
+ },
53
+ {
54
+ system: "Carbohydrate Metabolism",
55
+ high: ">4",
56
+ questions: 4,
57
+ result: 0,
58
+ maximum: 12,
59
+ },
60
+ {
61
+ system: "Immune System",
62
+ high: ">3",
63
+ questions: 4,
64
+ result: 0,
65
+ maximum: 10,
66
+ },
67
+ { system: "Lifestyle", high: ">7", questions: 7, result: 0, maximum: 7 },
68
+ {
69
+ system: "Hormonal Health Female",
70
+ high: null,
71
+ questions: null,
72
+ result: 0,
73
+ maximum: null,
74
+ },
75
+ {
76
+ system: "Hormonal Health Male",
77
+ high: null,
78
+ questions: null,
79
+ result: 0,
80
+ maximum: null,
81
+ },
82
+ ],
83
+ meta: {
84
+ patientName: "Final Test 3",
85
+ age: "37",
86
+ bmi: "23.67",
87
+ },
88
+ };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Real data — maps the `doctorScores` payload cerbo sends for a Maison Sante
93
+ // doctor PDF onto the row shape above. Cerbo computes each section's total and
94
+ // maximum from the live wellbeing metadata, so its numbers win over the dummy
95
+ // maxima here (which came from the RESULTS KEY sheet and disagree with the
96
+ // doctor questionnaire spreadsheet on Essential Nutrients and Lifestyle).
97
+ // ---------------------------------------------------------------------------
98
+
99
+ // Section labels of the reference design, keyed by cerbo's stable symbol. Keeps
100
+ // the table wording stable even though the metadata element names differ
101
+ // (e.g. "IMMUNE SYSTEM", "LIFESTYLE").
102
+ var SYSTEM_LABELS = {
103
+ "stomach-and-gi-tract": "STOMACH and GI (Gastrointestinal) Tract",
104
+ "small-intestine": "Small Intestine",
105
+ "infections-invasions-of-the-gi":
106
+ "Infections/Invasions of the GI (Gastrointestinal Tract)",
107
+ "large-intestine": "Large Intestine",
108
+ "liver-and-gallbladder": "Liver and Gallbladder",
109
+ "essential-nutrients": "Essential Nutrients",
110
+ "carbohydrate-metabolism": "Carbohydrate Metabolism",
111
+ "immune-system": "Immune System",
112
+ "lifestyle": "Lifestyle",
113
+ };
114
+
115
+ function buildRowsFromDoctorScores(doctorScores, meta) {
116
+ var groups = (doctorScores && doctorScores.groups) || [];
117
+ if (!groups.length) return null;
118
+
119
+ var rows = groups.map(function (group) {
120
+ return {
121
+ system: SYSTEM_LABELS[group.symbol] || group.name || group.symbol,
122
+ high:
123
+ group.highThreshold === null || group.highThreshold === undefined
124
+ ? null
125
+ : ">" + group.highThreshold,
126
+ questions:
127
+ group.questionCount === undefined ? null : group.questionCount,
128
+ result: group.total || 0,
129
+ maximum: group.max === undefined ? null : group.max,
130
+ };
131
+ });
132
+
133
+ // Hormonal Health is never scored (no maximum in the spreadsheet either); the
134
+ // rows exist so the doctor can see the questions were asked.
135
+ rows.push({
136
+ system: "Hormonal Health Female",
137
+ high: null,
138
+ questions: null,
139
+ result: 0,
140
+ maximum: null,
141
+ });
142
+ rows.push({
143
+ system: "Hormonal Health Male",
144
+ high: null,
145
+ questions: null,
146
+ result: 0,
147
+ maximum: null,
148
+ });
149
+
150
+ return { rows: rows, meta: meta || {} };
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Helpers
155
+ // ---------------------------------------------------------------------------
156
+ function fmt(val, decimals) {
157
+ if (val === null || val === undefined) return "\u2014"; // em dash
158
+ if (typeof val === "number") {
159
+ const rounded =
160
+ Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals);
161
+ return String(rounded);
162
+ }
163
+ return String(val);
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Single render — faithful to the reference image, Maison Sante colours.
168
+ //
169
+ // Structure:
170
+ // ┌──────────────────────────────────────────────────────┐
171
+ // │ Key to the Big Integral Questionnaire [title bar] │
172
+ // ├──────────────────────────────────────────────────────┤
173
+ // │ Client/Patient Full Name … Age … BMI … │
174
+ // ├──────────────────────────────────────────────────────┤
175
+ // │ SYSTEM SCORES [subheader band] │
176
+ // ├────────────┬──────┬─────────┬────────┬───────┬──┬───┤
177
+ // │ SYSTEM │ High │Questions│ Result │Maximum│/3 │Cmt│
178
+ // ├────────────┼──────┼─────────┼────────┼───────┼──┼───┤
179
+ // │ ...rows... │
180
+ // └──────────────────────────────────────────────────────┘
181
+ // ---------------------------------------------------------------------------
182
+ function renderTable(rows, meta, cfg) {
183
+ const c = cfg.colors;
184
+ const title = cfg.title || "Key to the Big Integral Questionnaire";
185
+
186
+ // ── patient info bar ──────────────────────────────────────────────────
187
+ const infoBar = `
188
+ <div style="background:#ffffff;border-bottom:1px solid ${c.rowBorder};">
189
+ <table style="border-collapse:collapse;">
190
+ <tr>
191
+ <td style="padding:7px 40px 7px 14px;font-size:9px;color:#1a1a1a;white-space:nowrap;vertical-align:middle;">
192
+ <span style="color:${c.headerBg};font-weight:700;">Client/Patient Full Name</span>
193
+ &nbsp;&nbsp;&nbsp;${meta.patientName}
194
+ </td>
195
+ <td style="padding:7px 40px 7px 0;font-size:9px;color:#1a1a1a;white-space:nowrap;vertical-align:middle;">
196
+ <span style="color:${c.headerBg};font-weight:700;">Age</span>
197
+ &nbsp;&nbsp;&nbsp;${meta.age}
198
+ </td>
199
+ <td style="padding:7px 0 7px 0;font-size:9px;color:#1a1a1a;white-space:nowrap;vertical-align:middle;">
200
+ <span style="color:${c.headerBg};font-weight:700;">BMI</span>
201
+ &nbsp;&nbsp;&nbsp;${meta.bmi}
202
+ </td>
203
+ </tr>
204
+ </table>
205
+ </div>`;
206
+
207
+ // ── "SYSTEM SCORES" subheader band ───────────────────────────────────
208
+ const subheaderBg = c.subheaderBg || "#1B4080";
209
+ const subheaderBand = `
210
+ <div style="
211
+ padding: 5px 14px;
212
+ background: ${subheaderBg};
213
+ color: #ffffff;
214
+ font-size: 8.5px;
215
+ font-weight: 700;
216
+ letter-spacing: 0.8px;
217
+ text-transform: uppercase;
218
+ ">SYSTEM SCORES</div>`;
219
+
220
+ // ── column widths ──────────────────────────────────────────────────────
221
+ const COL = {
222
+ system: "width:34%;text-align:left;",
223
+ high: "width:8%; text-align:center;",
224
+ questions: "width:9%; text-align:center;",
225
+ result: "width:8%; text-align:center;",
226
+ maximum: "width:9%; text-align:center;",
227
+ perThree: "width:8%; text-align:center;",
228
+ comments: "width:24%;text-align:left;",
229
+ };
230
+
231
+ const TH_BASE = `padding:6px 14px;font-size:8px;font-weight:700;color:#ffffff;
232
+ background:${c.headerBg};border-bottom:2px solid rgba(255,255,255,0.2);`;
233
+
234
+ const th = (label, colKey) =>
235
+ `<th style="${TH_BASE}${COL[colKey]}">${label}</th>`;
236
+
237
+ const tableHeader = `
238
+ <tr>
239
+ ${th("SYSTEM", "system")}
240
+ ${th("High", "high")}
241
+ ${th("Questions", "questions")}
242
+ ${th("Result", "result")}
243
+ ${th("Maximum", "maximum")}
244
+ ${th("/3", "perThree")}
245
+ ${th("Comments", "comments")}
246
+ </tr>`;
247
+
248
+ // ── data rows ─────────────────────────────────────────────────────────
249
+ const TD = (content, colKey, extra = "") =>
250
+ `<td style="padding:5px 14px;font-size:8.5px;${COL[colKey]}border-bottom:1px solid ${c.rowBorder};${extra}">${content}</td>`;
251
+
252
+ const dataRows = rows
253
+ .map((row, i) => {
254
+ const hasThreshold = row.high !== null && row.high !== undefined;
255
+ const hasQuantity = row.questions !== null && row.questions !== undefined;
256
+ const hasMaximum = row.maximum !== null && row.maximum !== undefined;
257
+
258
+ const highDisplay = hasThreshold
259
+ ? `<span style="color:${c.elevatedText};font-weight:600;">${row.high}</span>`
260
+ : "\u2014";
261
+
262
+ const questionsDisplay = hasQuantity ? String(row.questions) : "\u2014";
263
+ const resultDisplay = String(row.result);
264
+ const maximumDisplay = hasMaximum ? fmt(row.maximum, 0) : "\u2014";
265
+ const perThreeDisplay = hasMaximum ? fmt(row.maximum / 3, 2) : "\u2014";
266
+
267
+ const rowBg = i % 2 === 0 ? "#ffffff" : "rgba(15, 32, 98, 0.03)";
268
+
269
+ return `
270
+ <tr style="background:${rowBg};">
271
+ ${TD(`<span style="color:#1a1a1a;">${row.system}</span>`, "system")}
272
+ ${TD(highDisplay, "high")}
273
+ ${TD(questionsDisplay, "questions")}
274
+ ${TD(resultDisplay, "result", `font-weight:600;color:#444;`)}
275
+ ${TD(maximumDisplay, "maximum")}
276
+ ${TD(perThreeDisplay, "perThree")}
277
+ ${TD("", "comments")}
278
+ </tr>`;
279
+ })
280
+ .join("");
281
+
282
+ // ── assemble ──────────────────────────────────────────────────────────
283
+ // Outer wrapper uses padding-top instead of an empty spacer div, because
284
+ // empty divs with only `height` get collapsed by wkhtmltopdf. Padding
285
+ // always renders.
286
+ return `
287
+ <div style="padding-top:25px;font-family:'Open Sans',sans-serif;">
288
+ <div class="biq-wrapper" style="
289
+ font-family: 'Open Sans', sans-serif;
290
+ page-break-inside: avoid;
291
+ border-radius: 4px;
292
+ overflow: hidden;
293
+ border: 1px solid ${c.rowBorder};
294
+ ">
295
+ <!-- title bar -->
296
+ <div style="
297
+ background: ${c.headerBg};
298
+ border-radius: 4px 4px 0 0;
299
+ color: #ffffff;
300
+ padding: 9px 14px;
301
+ font-size: 11px;
302
+ font-weight: 700;
303
+ letter-spacing: 0.3px;
304
+ ">${title}</div>
305
+
306
+ ${infoBar}
307
+ ${subheaderBand}
308
+
309
+ <table style="width:100%;border-collapse:collapse;table-layout:fixed;">
310
+ <thead>${tableHeader}</thead>
311
+ <tbody>${dataRows}</tbody>
312
+ </table>
313
+ </div>
314
+ </div>`;
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Public entry
319
+ // ---------------------------------------------------------------------------
320
+ /**
321
+ * Render the Big Integral Questionnaire section for Maison Sante doctor PDFs.
322
+ *
323
+ * @param {object} opts
324
+ * @param {object} opts.clientConfig - The full maisonsante.json config object
325
+ * @param {object} opts.localeService - localeService with .t(key) method (unused for now; strings are hardcoded to match the reference design)
326
+ * @returns {string} HTML string to inject into #big-integral-section
327
+ */
328
+ function renderBigIntegralQuestionnaire({ clientConfig, doctorScores, meta }) {
329
+ const cfg = (clientConfig && clientConfig.big_integral_questionnaire) || {};
330
+
331
+ const colors = Object.assign(
332
+ {
333
+ headerBg: "#0F2062",
334
+ subheaderBg: "#1B4080",
335
+ headerText: "#FFFFFF",
336
+ rowBorder: "rgba(15, 32, 98, 0.12)",
337
+ elevatedText: "#A43B2F",
338
+ },
339
+ cfg.colors || {},
340
+ );
341
+
342
+ const resolvedCfg = Object.assign({}, cfg, { colors });
343
+
344
+ // Real payload when cerbo sent one; the dummy table is the design placeholder
345
+ // and is only reached when doctorScores is absent (e.g. an unfinished quiz).
346
+ const real = buildRowsFromDoctorScores(doctorScores, meta);
347
+ if (!real) {
348
+ console.log("[BigIntegral] no doctorScores in payload — rendering placeholder table");
349
+ } else {
350
+ console.log("[BigIntegral] rendering " + real.rows.length + " rows from doctorScores");
351
+ }
352
+ const source = real || getDummyData();
353
+ const allRows = source.rows;
354
+ const resolvedMeta = source.meta;
355
+
356
+ const rows = allRows.filter((row) => {
357
+ if (
358
+ row.system === "Hormonal Health Female" &&
359
+ cfg.showHormonalFemale === false
360
+ )
361
+ return false;
362
+ if (row.system === "Hormonal Health Male" && cfg.showHormonalMale === false)
363
+ return false;
364
+ return true;
365
+ });
366
+
367
+ return renderTable(rows, resolvedMeta, resolvedCfg);
368
+ }
369
+
370
+ module.exports = { renderBigIntegralQuestionnaire };