@medicus.ai/medicus-report-pdf-generator 1.3.14 → 1.3.16

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 (169) hide show
  1. package/.vscode/settings.json +3 -3
  2. package/README.md +26 -26
  3. package/app/i18n.config.js +20 -20
  4. package/app/i18n_wellbeing.config.js +20 -20
  5. package/app/services/localeService.js +28 -28
  6. package/assets/Mediclinic/css/nasco_report.css +6297 -6297
  7. package/assets/Mediclinic/css/nasco_report_rtl.css +851 -851
  8. package/assets/Mediclinic/css/style.css +33 -33
  9. package/assets/Mediclinic/js/js.js +112 -112
  10. package/assets/Mediclinic/js/raphael.js +8437 -8437
  11. package/assets/arabic.css +275 -275
  12. package/assets/charts.min.js +7 -7
  13. package/assets/corporate_report/css/report.css +582 -582
  14. package/assets/corporate_report/css/report_rtl.css +144 -144
  15. package/assets/corporate_report/js/js.js +14 -14
  16. package/assets/data/data.json +124 -124
  17. package/assets/data/data2.json +675 -675
  18. package/assets/data/pdf_data_with_history.json +1255 -1255
  19. package/assets/doctor-note-insight-icon.svg +11 -11
  20. package/assets/imc/css/style.css +61 -61
  21. package/assets/jquery-1.4.min.js +151 -151
  22. package/assets/jquery-2.1.0.min.js +3 -3
  23. package/assets/js.js +201 -201
  24. package/assets/medicus_pdf/arabic.css +444 -444
  25. package/assets/medicus_pdf/styles.css +2427 -2427
  26. package/assets/print.css +4 -4
  27. package/assets/qr-code-style.css +68 -68
  28. package/assets/qrcode.min.js +3 -3
  29. package/assets/raphael-min.js +10 -10
  30. package/assets/sanusx/css/sanusx_report.css +707 -707
  31. package/assets/sizing.js +2 -2
  32. package/assets/styles.css +1545 -1545
  33. package/assets/styles.min.css +1358 -1358
  34. package/assets/translation.js +30 -30
  35. package/assets/wellbeing/css/nasco_report.css +3472 -3472
  36. package/assets/wellbeing/css/nasco_report_rtl.css +679 -679
  37. package/assets/wellbeing/js/js.js +111 -111
  38. package/assets/wellbeing/js/raphael.js +8437 -8437
  39. package/base64/cairo-font.js +2 -2
  40. package/config/Mediclinic.json +37 -37
  41. package/config/Najeeb.ai.json +35 -35
  42. package/config/Pha.json +64 -64
  43. package/config/bionext.json +37 -37
  44. package/config/default.json +35 -35
  45. package/config/diagnostikare.json +34 -34
  46. package/config/maisonsante.json +39 -39
  47. package/config/nasco.json +35 -35
  48. package/config/sanitas.json +35 -35
  49. package/config/sanusx.json +34 -34
  50. package/config/test.txt +4 -4
  51. package/data converter.js +53 -53
  52. package/downloadfile.js +6 -6
  53. package/example.txt +1188 -1188
  54. package/index.js +481 -481
  55. package/lib/big_integral_questionnaire.js +367 -298
  56. package/lib/corporate_report_generator.js +399 -399
  57. package/lib/pdf_generator.min.js +525 -525
  58. package/lib/sanusx_report_generator.js +533 -533
  59. package/lib/sendEmail.js +343 -343
  60. package/lib/template.js +2227 -2227
  61. package/lib/wellbeing_report_generator.js +2164 -2134
  62. package/locales/ar-AE.json +169 -169
  63. package/locales/ar-SA.json +169 -169
  64. package/locales/ar.json +397 -397
  65. package/locales/de.json +169 -169
  66. package/locales/en.json +229 -229
  67. package/locales/fr.json +168 -168
  68. package/locales/pt.json +165 -165
  69. package/locales/wellbeing/ar-AE.json +149 -149
  70. package/locales/wellbeing/ar-SA.json +257 -257
  71. package/locales/wellbeing/de.json +301 -301
  72. package/locales/wellbeing/en.json +423 -423
  73. package/locales/wellbeing/es.json +133 -133
  74. package/locales/wellbeing/fr.json +121 -121
  75. package/locales/wellbeing/it-IT.json +121 -121
  76. package/locales/wellbeing/pt.json +121 -121
  77. package/locales/wellbeing/tr.json +121 -121
  78. package/locales/wellbeing/zh-CN.json +121 -121
  79. package/locales/zh-CN.json +165 -165
  80. package/package.json +47 -47
  81. package/preview-big-integral.js +75 -75
  82. package/run.js +15 -15
  83. package/templates/Mediclinic/blocks/first-section.html +205 -205
  84. package/templates/Mediclinic/blocks/footer.html +7 -7
  85. package/templates/Mediclinic/blocks/header-template.html +5 -5
  86. package/templates/Mediclinic/blocks/header.html +20 -20
  87. package/templates/Mediclinic/blocks/patient-table.html +38 -38
  88. package/templates/Mediclinic/blocks/pdf-header.html +9 -9
  89. package/templates/Mediclinic/blocks/tips.html +8 -8
  90. package/templates/Mediclinic/ltr_no_pages.html +37 -37
  91. package/templates/Mediclinic/rtl_no_pages.html +42 -42
  92. package/templates/Mediclinic/wellbeing_template.html +41 -41
  93. package/templates/Pha/blocks/first-section.html +207 -207
  94. package/templates/Pha/blocks/footer.html +7 -7
  95. package/templates/Pha/blocks/header-template.html +5 -5
  96. package/templates/Pha/blocks/header.html +20 -20
  97. package/templates/Pha/blocks/patient-table.html +38 -38
  98. package/templates/Pha/blocks/pdf-header.html +8 -8
  99. package/templates/Pha/blocks/tips.html +8 -8
  100. package/templates/Pha/ltr_no_pages.html +37 -37
  101. package/templates/Pha/rtl_no_pages.html +42 -42
  102. package/templates/Pha/wellbeing_template.html +41 -41
  103. package/templates/base.html +23 -23
  104. package/templates/blocks/ar-first-page-header.html +68 -68
  105. package/templates/blocks/ar-footer.html +54 -54
  106. package/templates/blocks/ar-header.html +67 -67
  107. package/templates/blocks/biomarker-compact-ar.html +27 -27
  108. package/templates/blocks/biomarker-compact.html +27 -27
  109. package/templates/blocks/biomarker-details.html +41 -41
  110. package/templates/blocks/biomarker-insight.html +22 -22
  111. package/templates/blocks/biomarker-min.html +9 -9
  112. package/templates/blocks/doctor-note.html +11 -11
  113. package/templates/blocks/footer.html +35 -35
  114. package/templates/blocks/header-first-page.html +46 -46
  115. package/templates/blocks/header.html +49 -49
  116. package/templates/blocks/panel-compact.html +38 -38
  117. package/templates/blocks/panel-details.html +16 -16
  118. package/templates/blocks/report-summary.html +16 -16
  119. package/templates/blocks/section-title.html +4 -4
  120. package/templates/blocks/signature.html +16 -16
  121. package/templates/blocks/summary-insight.html +15 -15
  122. package/templates/blocks/summary.html +9 -9
  123. package/templates/corporate_report/cover_page.html +23 -23
  124. package/templates/corporate_report/footer.html +9 -9
  125. package/templates/corporate_report/ltr_no_pages.html +20 -20
  126. package/templates/corporate_report/page.html +24 -24
  127. package/templates/corporate_report/participant_analytics.html +5 -5
  128. package/templates/corporate_report/rtl_no_pages.html +20 -20
  129. package/templates/empty.html +10 -10
  130. package/templates/first_page_head.html +157 -157
  131. package/templates/imc/first-header-template.html +53 -53
  132. package/templates/ltr.html +1368 -1368
  133. package/templates/ltr_no_pages.html +89 -89
  134. package/templates/maisonsante/blocks/first-section.html +151 -207
  135. package/templates/maisonsante/blocks/footer.html +7 -7
  136. package/templates/maisonsante/blocks/header-template.html +5 -5
  137. package/templates/maisonsante/blocks/header.html +20 -20
  138. package/templates/maisonsante/blocks/patient-table.html +38 -38
  139. package/templates/maisonsante/blocks/pdf-header.html +8 -8
  140. package/templates/maisonsante/blocks/tips.html +8 -8
  141. package/templates/maisonsante/ltr_no_pages.html +38 -38
  142. package/templates/maisonsante/rtl_no_pages.html +43 -43
  143. package/templates/maisonsante/wellbeing_template.html +41 -41
  144. package/templates/no_pages.html +1224 -1224
  145. package/templates/popup/popup-template.html +129 -129
  146. package/templates/rtl_no_pages.html +92 -92
  147. package/templates/sanusx/blocks/footer.html +6 -6
  148. package/templates/sanusx/blocks/header.html +20 -20
  149. package/templates/sanusx/blocks/personal-details.html +123 -123
  150. package/templates/sanusx/blocks/predictions-section.html +57 -57
  151. package/templates/sanusx/blocks/recommendations-section.html +6 -6
  152. package/templates/sanusx/blocks/super-power-section.html +29 -29
  153. package/templates/sanusx/blocks/tips.html +8 -8
  154. package/templates/sanusx/ltr_no_pages.html +26 -26
  155. package/templates/sanusx/rtl_no_pages.html +36 -36
  156. package/templates/template.html +1377 -1377
  157. package/templates/wellbeing/blocks/first-section.html +106 -106
  158. package/templates/wellbeing/blocks/footer.html +7 -7
  159. package/templates/wellbeing/blocks/header.html +37 -37
  160. package/templates/wellbeing/blocks/tips.html +8 -8
  161. package/templates/wellbeing/ltr_no_pages.html +35 -35
  162. package/templates/wellbeing/rtl_no_pages.html +36 -36
  163. package/templates/wellbeing/wellbeing_template.html +43 -43
  164. package/test.js +74 -74
  165. package/.claude/settings.local.json +0 -16
  166. package/testing-reports/mediclinic/arabic-data.js +0 -439
  167. package/testing-reports/mediclinic/doctor-data.js +0 -523
  168. package/testing-reports/mediclinic/with-wellbeing.js +0 -415
  169. package/testing-reports/pha/notes-example.js +0 -397
@@ -1,298 +1,367 @@
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:45%;text-align:left;",
223
+ high: "width:10%;text-align:center;",
224
+ questions: "width:12%;text-align:center;",
225
+ result: "width:10%;text-align:center;",
226
+ maximum: "width:12%;text-align:center;",
227
+ perThree: "width:11%;text-align:center;",
228
+ };
229
+
230
+ const TH_BASE = `padding:6px 14px;font-size:8px;font-weight:700;color:#ffffff;
231
+ background:${c.headerBg};border-bottom:2px solid rgba(255,255,255,0.2);`;
232
+
233
+ const th = (label, colKey) =>
234
+ `<th style="${TH_BASE}${COL[colKey]}">${label}</th>`;
235
+
236
+ const tableHeader = `
237
+ <tr>
238
+ ${th("SYSTEM", "system")}
239
+ ${th("High", "high")}
240
+ ${th("Questions", "questions")}
241
+ ${th("Result", "result")}
242
+ ${th("Maximum", "maximum")}
243
+ ${th("/3", "perThree")}
244
+ </tr>`;
245
+
246
+ // ── data rows ─────────────────────────────────────────────────────────
247
+ const TD = (content, colKey, extra = "") =>
248
+ `<td style="padding:5px 14px;font-size:8.5px;${COL[colKey]}border-bottom:1px solid ${c.rowBorder};${extra}">${content}</td>`;
249
+
250
+ const dataRows = rows
251
+ .map((row, i) => {
252
+ const hasThreshold = row.high !== null && row.high !== undefined;
253
+ const hasQuantity = row.questions !== null && row.questions !== undefined;
254
+ const hasMaximum = row.maximum !== null && row.maximum !== undefined;
255
+
256
+ const highDisplay = hasThreshold
257
+ ? `<span style="color:${c.elevatedText};font-weight:600;">${row.high}</span>`
258
+ : "\u2014";
259
+
260
+ const questionsDisplay = hasQuantity ? String(row.questions) : "\u2014";
261
+ const resultDisplay = String(row.result);
262
+ const maximumDisplay = hasMaximum ? fmt(row.maximum, 0) : "\u2014";
263
+ const perThreeDisplay = hasMaximum ? fmt(row.maximum / 3, 2) : "\u2014";
264
+
265
+ const rowBg = i % 2 === 0 ? "#ffffff" : "rgba(15, 32, 98, 0.03)";
266
+
267
+ return `
268
+ <tr style="background:${rowBg};">
269
+ ${TD(`<span style="color:#1a1a1a;">${row.system}</span>`, "system")}
270
+ ${TD(highDisplay, "high")}
271
+ ${TD(questionsDisplay, "questions")}
272
+ ${TD(resultDisplay, "result", `font-weight:600;color:#444;`)}
273
+ ${TD(maximumDisplay, "maximum")}
274
+ ${TD(perThreeDisplay, "perThree")}
275
+ </tr>`;
276
+ })
277
+ .join("");
278
+
279
+ // ── assemble ──────────────────────────────────────────────────────────
280
+ // Outer wrapper uses padding-top instead of an empty spacer div, because
281
+ // empty divs with only `height` get collapsed by wkhtmltopdf. Padding
282
+ // always renders.
283
+ return `
284
+ <div style="padding-top:25px;font-family:'Open Sans',sans-serif;">
285
+ <div class="biq-wrapper" style="
286
+ font-family: 'Open Sans', sans-serif;
287
+ page-break-inside: avoid;
288
+ border-radius: 4px;
289
+ overflow: hidden;
290
+ border: 1px solid ${c.rowBorder};
291
+ ">
292
+ <!-- title bar -->
293
+ <div style="
294
+ background: ${c.headerBg};
295
+ border-radius: 4px 4px 0 0;
296
+ color: #ffffff;
297
+ padding: 9px 14px;
298
+ font-size: 11px;
299
+ font-weight: 700;
300
+ letter-spacing: 0.3px;
301
+ ">${title}</div>
302
+
303
+ ${infoBar}
304
+ ${subheaderBand}
305
+
306
+ <table style="width:100%;border-collapse:collapse;table-layout:fixed;">
307
+ <thead>${tableHeader}</thead>
308
+ <tbody>${dataRows}</tbody>
309
+ </table>
310
+ </div>
311
+ </div>`;
312
+ }
313
+
314
+ // ---------------------------------------------------------------------------
315
+ // Public entry
316
+ // ---------------------------------------------------------------------------
317
+ /**
318
+ * Render the Big Integral Questionnaire section for Maison Sante doctor PDFs.
319
+ *
320
+ * @param {object} opts
321
+ * @param {object} opts.clientConfig - The full maisonsante.json config object
322
+ * @param {object} opts.localeService - localeService with .t(key) method (unused for now; strings are hardcoded to match the reference design)
323
+ * @returns {string} HTML string to inject into #big-integral-section
324
+ */
325
+ function renderBigIntegralQuestionnaire({ clientConfig, doctorScores, meta }) {
326
+ const cfg = (clientConfig && clientConfig.big_integral_questionnaire) || {};
327
+
328
+ const colors = Object.assign(
329
+ {
330
+ headerBg: "#0F2062",
331
+ subheaderBg: "#1B4080",
332
+ headerText: "#FFFFFF",
333
+ rowBorder: "rgba(15, 32, 98, 0.12)",
334
+ elevatedText: "#A43B2F",
335
+ },
336
+ cfg.colors || {},
337
+ );
338
+
339
+ const resolvedCfg = Object.assign({}, cfg, { colors });
340
+
341
+ // Real payload when cerbo sent one; the dummy table is the design placeholder
342
+ // and is only reached when doctorScores is absent (e.g. an unfinished quiz).
343
+ const real = buildRowsFromDoctorScores(doctorScores, meta);
344
+ if (!real) {
345
+ console.log("[BigIntegral] no doctorScores in payload — rendering placeholder table");
346
+ } else {
347
+ console.log("[BigIntegral] rendering " + real.rows.length + " rows from doctorScores");
348
+ }
349
+ const source = real || getDummyData();
350
+ const allRows = source.rows;
351
+ const resolvedMeta = source.meta;
352
+
353
+ const rows = allRows.filter((row) => {
354
+ if (
355
+ row.system === "Hormonal Health Female" &&
356
+ cfg.showHormonalFemale === false
357
+ )
358
+ return false;
359
+ if (row.system === "Hormonal Health Male" && cfg.showHormonalMale === false)
360
+ return false;
361
+ return true;
362
+ });
363
+
364
+ return renderTable(rows, resolvedMeta, resolvedCfg);
365
+ }
366
+
367
+ module.exports = { renderBigIntegralQuestionnaire };